Он работает на уровне сурсов (если точнее, то AST), используя внутренние API javac/ecj (публичное API не даёт редактировать дерево существующих сурсов, а только позволяет создавать новые).
Как раз то, что вы привели в примере, не будет валидно в случае мьютексов Раста.
Валидным тут будет:
// где-то
let data = Mutex::new(123);
// ...
// при необходимости использовать значение
// получили "умный указатель",
// дающий эксклюзивный доступ к объекту
let mut dataLocked = data.lock()?; // <lock()>
if *dataLocked != 0 {
*dataLocked /= 42;
}
// <unlock()> там, где заканчивается область видимости `dataLocked`
Непосредственно на data вы никакие операции с данными выполнять не можете.
Интересные вопросы. Постараюсь ответить по пунктам:
Traits приводит к тому, что разработчики библиотек решая простейшие задачи стремятся по максимуму использоваться всесь потенциал Traits из-за этого интерфейс библиотек многократно переусложнен относительно решаемой задачи.
Не совсем понимаю, какой вред носит реализация тех или иных трейтов библиотечным типом. Если некоторый тип T реализует трейт Foo, это влияет только дополняет то, что с ним можно делать, но никак -- по-моему -- не усложняет взаимодействие с ним. Если этот типаж Вам, как пользователю API, не нужен, то вы об этом даже и не задумываетесь. Если же он окажется нужным, то вам же будет плюсом то, что он реализован для данного типа. Как по мне, в C++, как раз, с этим хуже, потому что многие решения (точнее, все), завязанные на активном использовании статического полиморфизма, полагаются сугубо на около-утиную типизацию (утрированно), а именно наличие функций/полей/.., "похожих" на то, что ожидается. Тут и не получается получать реальные статические гарантии того, что это именно то, что нужно (например, у типа могут быть begin() и end()методы, но при этом он может не иметь никакого отношения к итераторам), и, при этом, нельзя реализовать полноценную совместимость между библиотеками, в случае, например, коллизий каких-либо имён (условно, libFoo требует метод ::std::string name(), а libBar -- Id name(); пример искуственный но суть, думаю, передаёт). Более того, многие опциональные реализации трейтов (например, для поддержки популярных, но не обязательных библиотек) во многих проектах спрятаны за feature-флагами, так что, если они вам не нужны, вы и не будете их включать.
Казалось бы Option/Result благое намерение но большинство кода , что я видел, выглядит как бесконечное unwrap()
Довольно странно. Не буду бросаться утверждениями, но, как мне кажется, то, что вы видели, это либо те случаи, когда известно, что ошибка невозможна, но, в общем случае, возвразается Result/Option (например, 127.0.0.1"::parse::<IpAddr>()), либо вы натыкались на неидеоматично написанный код.
Но все же непонятно зачем делать фичу которую большинство разработчиков не будут использовать
Как раз, большинство очень активно пользуется этим и пишет API именно под такую модель работы с ошибками.
Более того, существует прекрасный try-operator -- ?, который по смыслу (с некоторыми допущениями) превращает
fn foo() -> Result<String, BusinessError> {
let id = match bar() { // то, во что по смыслу превращается оператор
Ok(id) => id,
Err(ioError) => return BusinessError::from(ioError);
};
todo!("...")
}
fn bar() -> Result<u32, IoError> {
todo!("...")
}
За счёт этого, в большинстве случаев, проброс ошибки вверх по стеку -- это просто написание оператора-вопросика после нужного значения.
Касаемо panic. Не мог понять в go, так и в rust не могу зачем делать такую неудобную реализацию обратоки ошибок, когда уже сущесвует подход с try/catch, чем паника отличается по сути от exception кроме существенно менее удобного синстаксиса работы с ней, может кто ни будь пояснит?
Паники, в целом, не предназначены для обработки ошибок бизнес-логики. Идеология Result, которую уместно и в другие языки перетащить -- это то, что ожидаемые ошибки -- это такие же равносильные результаты вызова функции, как и успешные значения, и, при этом, взаимоисключающие и, соответственно, в большинстве случаев, нет смысла использовать для этого отдельный механизм.
Утрируя, мы ведь не пишем код вроде (псевдокод):
true doOperation() throws false { ... }
Потому что как true, так и false оба ожидаемые результаты вызова функции. Так и, для большинства сценариев, ошибочный результат -- это тоже результат, а не какой-то отдельный исключительный сценарий.
Более того, этот подход ещё и дешевле, потому что не приходится поддерживать инфраструктуру, связанную с заполнением стек-трейса и прочими вещами, которые в большинстве случаев не нужны: с болью привожу как антипример многое из API джавы, где есть Integer#parseInt(String), который преобразует строку к числу, но, если строка не число -- кидает исключение, хотя во многих сценариях разработчик ожидает, что пользователь введёт не число и ему нет смысла от полноценно созданного NumberFormatException с заполненным стек-стрейсом (что очень дорого), который он тут же хочет обработать:
int number;
try {
number = Integer.parseInt(userInput);
} catch (final NumberFormatException expected) {
number = 42; // `expected` при этом даже не используется
}
Аналогичный код на Расте:
let number = userInput.parse::<i32>().unwrap_or(42);
// здесь небольшое уточнение:
// `unwrap_or` не имеет никакого отношения к `unwrap`,
// а подобен джавовому Optional#orElse
И таких сценариев большинство.
Механизм же паник предназнчен именно для того, чтобы безопасно (точнее, корректно) сообщить о том, что какой-то относительно хлипкий инвариант был нарушен, но это не имеет какого-то отношения к логике программы и, скорее всего, не подразумевает дальнейшей обработки. Например, есть std::unreachable!, который нужен для того, чтобы помечать недостижимый (по мнению разработчика) код, который, в случае, если до него таки дошло исполнение, паникует. Обрабатывать панику тут, в оббщем случае, нет никакого смысла, потому что её первопричина -- логическая ошибка в коде самого разработчика.
Это такой аналог unchecked exceptions из той же Джавы.
Зачем добавлять метод, который никогда не должен использоваться и не имеет смысла? Причём, более того, который даже не является примером для разработчика, как писать подобный, потому что:
его даже в общих чертах нельзя описать в Enum
разработчик итак никогда его не должен (и не сможет) реализовать, потому что вручную от Enum наследоваться нельзя
В итоге наличие такого метода вводило бы лишь в заблуждение.
В пределах Enum его нельзя реализовать, потому что для него этот метод не имеет смысла. И его наличие там никакой пользы не принесёт, т.к. обращение непосредственно к нему - бессмысленно, а обращение к его тёзке в конкретном enum'е отношения к нему иметь никакого не будет.
Но какой в этом смысл? Для самого Enum его нельзя корректно определить (= он бессмысленен); а при "перекрытии" у нас просто будет идентичный по названию и сигнатуре, но, в общем, никак не связанный с предыдущим, другой метод .
В вызове unsafe-функции? - потому что это также unsafe действие.
В определении unsafe-функции? Потому что она ведёт к UB, если вызывающий не соблюл инвариант (неалиасность указателей).
Зачем в реализации свапа указатели?
Потому что существует реализация свапа для указателей, которая умеет делать это очень быстро при условии соблюдения инварианта. Этот инвариант гарантирован для &mut T, а, значит, нет необходимости делать повторную реализацию для него, когда можно воспользоваться той.
Вы этим примером только подтвердили тезис выше. Не способность языка выразить даже свап. В чём была проблема написать `tmp = a; a = b; b = tmp;`, как это написано в том же C++?
Почему же вы считаете, что это нельзя выразить формально на Расте?
Библиотечный примитив мог быть реализован и так, но (и это уже детали реализации) была возможность сделать это, используя unsafe.
Плохо ли это? По-моему, нет, потому что это никак не влияет на пользователя безопасной функции swap. Он наоборот получает в составе стандартной библиотеки необходимый примитив и не должен сам заботить себя необходимостью где-то вручную проверять его корректность или, вообще, предпринимать какие-либо действия для его работоспособности.
Что же касается тезиса о том, что стандартная библиотека написана с большим количеством unsafe, то в чём именно проблема этого? В Расте есть unsafe и никто этого не отрицает, но идея в том, что в клиентском коде его прекрасно получается избегать.
Иначе (далее утрированный пример) можно точно так же сказать, что почему-то выразить средствами C++ (практически любого ЯП, на самом деле) сложение двух целых чисел можно, но это будет очень медленно (цикл (который, как и случай со swap, может сам требовать некоторый более примитивный аналог самого себя, в данном случае для счётчика цикла)) либо небезопасно (машинно-специфичный АСМ с учётом размерности, эндианности etc, причём это ещё если данный ЯП такой инструмент даёт). А то, что для этого есть "особый оператор +" считать признаком слабости языка.
Или же говорить, что использование инлайн-АСМа это признак слабости языка.
У нас там ссылки. Зачем нам ссылки, какие-то гарантии, если после они никак не используются?
Очень даже используются, использование двух mut-ссылок гарантирует, что они не алиасятся, что и является соблюдением инварианта unsafe-вызова для полученных из них указателей, которое (соблюдение неалиасности ссылок) в свою очередь гарантирует нам компилятор.
Нет, unreachable_unchecked нет никакого смысла быть ансейф. Просто это взятая из ллвм/С++-семантика(builtin_unreachable) которая продуцирует уб.
Показанный далее пример так же builtin_unreachable-семантики, которому никак не требуется быть ансейф. unreachable не ансей ф.
Почему же нет никакого смысла? Это хинт компилятору (не важно, какой там бэкенд, LLVM, GCC, whatever), что данная ветка гарантированно (разработчиком) недостижима, и компилятор (снова же, я не ссылаюсь на какой-то конкретный, а говорю, в общем, что значит эта (псевдо)функция) имеет право рассматривать это, как истину в последней инстанции. Несоблюдение этого инварианта есть UB, и не потому, что "так где-то сделано", а потому что вся суть этой функции в том, чтобы дать строгую статически-недоказуемую гарантию. Отсутствие unsafe (то есть, UB при несоблюдении инварианта) значило бы, что даже если ветка была бы достигнута, она должна была бы как-то конкретно быть обработана, для чего и существует не-unsafe аналог, который паникует (можно провести аналогию с плюсовым исключением) при достижении.
Зачем множество раз повторять "компилятор", "гарантировтаь" и прочие базворды, если они не имеет отношения происходящему? Какой тут компилятор что гарантирует?
Конкретно в данном примере корректность условной деконструкции.
Здесь показана примитивная семантика зашития в if вытаскивания значения. Он ничего из себя не представляет и не является аналогом нормальных проверок. И уж тем более он не является бесплатным.
Конкретно конструкция if-let является общим инструментом сравнения с паттерном, а не каким-то частным случаем для Option (не говорю, что Вы подобное утверждали, но, на всякий случай, здесь уточню это, чтобы не было дальнейшей полемики на эту тему).
Причём, опять же, что плохого в том, что семантика примитивная? Она простая, и при этом позволяет чётко описать действие - это же (синтаксис) и входит в понятие инструментария языка, о котором шла речь изначально.
А что до бесплатности, то куда ему быть ещё бесплатнее? Это пара проверка+доставание, которая в том же C++ (да и, в целом, любом ЯП) одинаково дорогая. Причём тут, поскольку это часть синтаксиса языка, очевидно, что это одно из первых мест, которые оптимизируются во всю (относительно, условно, каких-то конкретных функций).
Нормально и как раз таки на уровне компилятора это реализовано в том же ts. Когда у нас есть полноценный type refinement, а не какая-то его случайная эмуляция.
Оно позволяет делать и if(!x) {}, позволяет делать if(x && y) и прочее. let if ничего этого не позволяет, либо надо костыль обёртки.
А что именно тут не так? Это и не type refinement, формально, а просто pattern matching. Хотя играет он тут ту же роль: ограничивает в пределах блока if множество значений value типом T (сахара ради, новая переменная внутри if-let может, иметь точно такое же название, как и изначальная переменная, а ля if let Some(value) = value).
Про .value() уже сказали. *x - это такая же ансейф-операция.
В том то и дело, что в C++ в абсолютно изначально корректном месте приходится использовать unsafe (иначе говоря, UB-провоцирующую) функцию тогда, когда она таковой не является. Вся причина в том, что нету той самой пары проверь+возьми одним "атомарным" выражением, и система типов действительно делает тот UB синтаксиески возможным.
Вы совершенно верно подметили ниже функциональный вариант с map, который не пользуется спросом. Функциональный вариант тут и менее читаем (в некоторых случаях, хотя бы из-за громоздкости синтаксиса лямбд) и менее удобен (банальный capturing ставит палки в колёса) да и, полагаю, не очень идиоматичен.
Вы выше, если не ошибаюсь, писали, что Раст не даёт ничего для безопасности с нулевой ценой, но ведь тут именно оно - операция, которая в C++ описывалась парой никак не связанных statement'ов, тут представлена одним, при этом не оставляя места даже для формального UB.
Поймать это, конечно же можно, потому как паники это С++-исключения из llvm, но нормальных средств для этого нет.
Суть паник именно в том и состоит, чтобы сообщить о том, что какой-то хрупкий (=недоказуемый статически) инвариант был нарушен, не провоцируя при этом UB, но использование их для какой-либо бизнес-логики это явный антипаттерн. Ровно как в C++ не стали бы (я полагаю) ловить какой-нибудь std::invalid_argument.
Тот же самый unwrap, как раз, не столь "идиоматичный" (в виду наличия всё того же if-let, match и кучи функциональных методов), но позволяет, с одной стороны, гарантированно со стороны разработчика, но с другой - без UB (а с паникой в случае его ошибки) описывать случай, когда он знает, что значение есть, хоть и представлено в Option (например, когда это некоторый трейт, который возвращает, в общем случае, опциональное значение, но для какого-то конкретного типа оно всегда присутствует, причём с окончательным прибытие never-type (!) многие места, где ныне unwrap, будут прекрасно безусловно (let вместо if-let) раскрываться для, к примеру, Result).
---
Что же касается того уточнения, что при помощи unsafe можно получить два алиасищихся &mut T, то это и есть то самое UB, которое должен избегать соблюдением инварианта разработчик, и если в unsafe-блоке некто сделал бяку и вернул в результате два некорректных mut-указателя (в данном случае, указывающих на пересекающуюся зону) то это и есть UB, и гарантий, когда именно оно выстрелит, нельзя дать. Иначе говоря, наша программа уже, формально, в некорректном состоянии, а не вызов swap является небезопасным.
Почему предыдущий комментатор написал "это идея Rust", хотя она таковой не является?
Не очень понимаю такой зацикленности на моей фразе про идею, учитывая то, что изначально дискуссия была о более прикладной составляющей (в моём понимании), а не том, кто первый что изобрёл (по крайней мере, в этой части обсуждения участвовать не хочется), но, хорошо, соглашусь, выразился неудачно: это не что-то, что изобрёл Раст, это подход, который существует давно, но конкретно в нём позволяет на (практически) одном и том же языке писать что safe, что unsafe код, и при этом safe подмножество не несёт за собой оверхеда, характерного для управляемых языков.
Доказывался другой тезис, в именно то, что производительная низкоуровневая реализация (swap_nonoverlapping_one реализован, в общем, подобно тому, что вы отправили) не мешает быть точно такому же эффективному безопасному интерфейсу.
Постойте, вы сами себе противоречите. Выше был упомянут swap, в который завтра кто-то может попробовать передать ссылки на один и тот же объект через unsafe(и получить то самое UB). Но swap почему-то безопасен, а optional без проверки нет.
Если речь об std::swap, то почему вы в этом и предыдущем сообщении говорите о том, что кто-то будет вызывать его из unsafe, если это safe функция? Ещё раз, swap делегирует к unsafe методу swap_nonoverlapping_one, который требует соблюдения строго инварианта, но пользователя это не волнует. Потому что на вызове swap этот инвариант итак гарантирован. Задача реализации swap - вызвать swap_nonoverlapping_one, потому что два алиасных &mut T быть не может, а вот для *mut T такие гарантии должен (для данной задачи) гарантировать разработчик. Но не разработчик клиентского кода, а разработчик реализации swap. Я же как разработчик просто пользуюсь swap из safe и не задумываюсь (утрированно) над тем, как он реализован, потому что я знаю, что вызывать его неправильно я чисто физически не могу.
Ещё раз: нельзя забыть написать .value() - это так же не скомпилируется, поскольку типы будут разные.
Полагаю, проблема в моей опечатке выше, речь шла о has_value() (как могло быть очевидно из того, что я упомянул его вместе с operator bool).
Переформулирую: речь шла о том, что в случае Раста разработчик не может вне unsafe контекста достать опциональное значение просто сказав, что он обещает. В коде на плюсах проблема в том, что проверка на наличие значение и его доставание могут быть как угодно размазаны по коду и в дальнейшем проверка может быть просто забыта (да и даже при написании разработчик мог её забыть). Инструментарий плюсов никак этому не помешает, кроме, быть может, линтеров, но реальной гарантии корректности у вас нет. В Расте же у Option операция проверки и доставания объединена, и вы просто никак синтаксически не можете выразить то, что вы достали без гарантии. Представленная выше if let - это один, из множества вариантов это сделать, но каждый из этих вариантов защищён от небезопасного разыменования тем, что его просто нельзя осуществить. Да, если у вас реально есть какая-то причина полагать, что разыменования возможно без проверки, то вы можете положиться сделать его без проверки, но тогда будьте добры взять ответственность на себя, сделав это через уже unsafe функцию (насколько я помню, таковая реализована даже как отдельный крейт), потому что тут вы берёте на себя ответственность за то, что значение есть. Причём, скорее всего, если у вас есть причины полагать, что в опциональном значении значение есть всегда, то может и вовсе не стоит тут использовать Option, а уместнее что-то другое? Ну а если это действительно экзотический случай и тут нужно так сделать, то тут уж пишите unsafe, говоря, что, да, ответственность на вас.
Иначе с чего вдруг компилятору (да и просто тому, кто работает с вашим кодом) быть уверенным, что разыменования tagged unionа без проверки типа корректно?
Как раз идея Раста в том, чтобы предоставить дешёвые (в идеале, с нулевой стоимостью) абстракции, которые при этом безопасны.
Достигается это именно путём того, что на низком уровне что-то (и то, относительно узкая часть, а именно "самые-самые" примитивы + интеграции с внешними функциями) реализовано с unsafe, однако предоставляемый интерфейс (в широком смысле) - safe.
#[inline]
pub const fn swap<T>(x: &mut T, y: &mut T) {
// SAFETY: the raw pointers have been created from safe mutable references satisfying all the
// constraints on `ptr::swap_nonoverlapping_one`
unsafe {
ptr::swap_nonoverlapping_one(x, y);
}
}
Небезопасный ptr::swap_nonoverlapping_one небезопасен по той причине, что требует, потому что (вероятно) производит разыменование сырых указателей, а, главное, требует того, чтобы эти области памяти, на которые они указывают, не пересекались. В то же самое время, borrowing checker не позволяет иметь два &mut T, ссылающихся на одну и ту же область, поэтому вызов небезопасного ptr::swap_nonoverlapping_one от них безопасен. Что же до эффективности, то никакого оверхеда здесь нет. Преобразование ссылки к указателю это классический no-op. Инлайн же этой функции точно так же тривиален и почти наверняка (формально, компилятор имеет право решить, что он не нужен, но тут вероятность подобного стремится к нулю) произойдёт.
Таким образом мы получили безопасный swap при этом обладающий всеми преимуществами, с точки зрения производительности, безопасного варианта. При этом разработчику, пользующемуся им, не важно, как он устроен под капотом, и при этом он никак не может вызывать эту функцию неправильно.
Идея unsafe не в том, что им помечено всё быстрое и волшебное. Unsafe является то, что ведёт к неопределённому поведению при нарушении инварианта, который компилятор не имеет возможности гарантировать.
Другой неплохой пример - std::hint::unreachable_unchecked: он небезопасен, потому что переносит ответственность на доказательство того, что данный блок недостижим, на разработчика, зато даёт право компилятору полагаться на то, что этот блок гарантированно недостижим, например (пусть функция rand(n) даёт случайное число от 1 до n):
Компилятор не имеет никакой возможности явно доказать, что числа кроме 0 и 1 невозможны, потому что это логика вашей rand, но вы можете сделать это, взяв ответственность за ошибку в случае, если это не так, на себя, причём строго в пределах блока unsafe. В то же самое время, если вы "не, уверены" есть и не-unsafe аналог который вставляет код для паники в место своего использования, хоть он и не даёт компилятору просто так вырезать эту ветку. Причём, в действительности, компилятор, быть может, и в состоянии сам понять, что rand(2) не может вернуть ничего, кроме 0 и 1 и в таком случае и вариант с паникой без проблем будет вырезан.
Ну и другой пример для того, чтобы показать, что то, что в Расте safe, в целом, сделано наиболее дёшево, и аналогичный код на (например) C++ был бы ни чуть не быстрее: великий и могучий Option<T>. Самый близкий аналог на плюсах - std::optional<T>. И классическая же операция дереференса. Что в расте, что в плюсах, если вы собираетесь достать из него T, вам необходимо проверить, есть ли он там вообще. В C++ для этого используется `operator bool()` или value(), который можно банально забыть написать или при рефакторинге забыть изменить. Иначе говоря, проблема подхода там - неатомарность операций:
if (opt) foo(*opt);
Неатомарность в том значении, что компилятор не сможет никак гарантированно вам помочь, если вы забудете выполнить эту проверку. Может (i.e расширения), но не обязан, потому что с точки зрения языка код без проверки всё так же корректно. А плохо это потому что "быстрый" operator * ведёт к UB, если значение отсутствует.
Раст же даёт инструментарий (причём, довольно разнообразный), при котором чисто аналогичный код атомарен:
if let Some(value) = opt {
foo(value)
}
Как видно, код на обоих языках делает абсолютно одно и то же, но то, что в плюсах таит UB, которое может быть никем на замечено, в Расте обёрнуто в безопасную обёртку, которая при этом ничуть не проигрывает по производительности.
Важно отметить, что метод fork() отправляет задачу в какой-либо поток, но при этом не запускает её выполнения.
На самом деле, fork() отправляя задачу в поток на выполнение (пусть и без гарантии того, когда именно он ей займётся) и вызов join() не обязателен для того, чтобы она была завершена, что можно увидеть на следующем синтетическом примере:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RecursiveAction;
class Scratch {
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
new RecursiveAction() {
@Override
protected void compute() {
latch.countDown();
}
}.fork();
latch.await();
System.out.println("Completed without join()");
}
}
Последняя строчка успешно вызывается, что было бы невозможно, если бы задача не выполнилась.
Хорошая статья с точки зрения простоты объяснения.
Небольшое уточнение:
То есть при вызове метода fork() задача не «выполнила сама себя» магическим образом, а была выполнена в том же потоке из которого и был вызван данный метод.
В данном случае, да, но есть уточнение на счёт случая вызова из потока не относящегося ни к какому FJP:
Если смотреть в Джавадок метода fork(), то мы увидим, что он по возможности будет использовать текущий пул (пул, к которому "относится" вызывающий поток), но в противном случае будет использовать глобальный. Например, поведение с commonPool можно было бы понаблюдать в Вашем примере с вызовом fork() напрямую из main.
Мелочь, просто чтобы не оставлять неочевидных моментов)
Ну почему же? Всё тот же Spring умеет в инициализацию через конструктор, либо, что ещё более приятно, static factory method. Единственное, что, по-моему, может мешать этому — циклические зависимости, но, это как раз чаще признак того, что что-то не так.
Неициализированные поля, по-моему, в целом проблема дизайна, потому что всё подобное (поля) стоит делать final, инициализируя при создании объекта. Экзотику с не очень удачным DI, вроде оного в JFX, стоит рассматривать, скорее, как неудачное решение.
Для локальных переменных они, как раз, не нужны, потому что там nullability однозначно выводится.
Тут, увы, да, но в пределах API, скорее, nullа будут избегать (снова же, при удачном дизайне). Классический пример, пустая коллекция лучше nullовой.
неэффективный — один ClassLoader на группу классов, которая может быть отгружена. В случае с классами плагина это не столь критично, поскольку если и есть необходимость их отгрузки, то, скорее всего, всех вместе.
Unsafe-way: Unsafe#defineAnonymousClass(..) — способ, для которого, очевидно, тем более никто не даёт гарантий работоспособности и безопасности, но который более подходит для варианта, когда нужно загружать класс и отгружать, по возможности, как только у него нет экземпляров. Как пример, он используется для сгенерированный LambdaMetafactory и StringConcatFactory классов лямбд и соединителей строк, соответственно (актуально для текущей OpenJDK), а также в Nashorn для загрузки скомпилированных скриптов.
На моём опыте, первый случай, как и было сказано, уместен для случаев, когда группа классов загружается и может быть в каком-то случае отгружена (eg всё те же плагины), а второй — для динамически генерируемых классов с жизненным циклом таким же, как и жизненный цикл их экземпляров (eg, я в одном из проектов для генерации динамических строк, на основе шаблонов, в рантайме генерирую класс с агрессивным инлайнингом, который, однако, нужен ровно столько, сколько существует его экземпляр).
Порой приходится, если описание конфигурации плагина, необходимой для его загрузки, записано, допустим, в аннотациях в некоторых классах плагина, однако при этом нужно избегать их загрузки.
Например, некая аннотация, указываются на то, какую зависимость нужно загрузить до данной или, например, просто указывающая на entry-point плагина.
По-моему, очень странное мнение у Ваших коллег.
На то несколько причин:
В случае какого-либо изменения поведения, будь то pre-conditions или смена метода хранения этих данных, вам придётся обновлять код везде, где присутствует прямой доступ, зачастую дублируя логику (e.g перед каждым foo.bar = baz; какой-нибудь if (baz != null && foo.qux/* аналогично bad practice */).willNotBreakWith(baz))).
Во-вторых, таким образом вы привязываетесь к конкретным классам, что усложняет значительно юнит-тестирование, потому что вы не можете даже задать логику работы с объектом через интерфейс.
При этом, любой рефакторинг класса, имеющего эти поля, может повлечь ломание всего с ним связанного (и даже если в пределах проекта с этим может справиться IDE, то внешние пользователи Вашего API сломаются).
Возможно, конечно, они говорили о свойствах объектов, вроде того, как Kotlin даёт доступ к ним через foo.bar, а не foo.bar(), однако они, в свою очередь, также реализованы через (синтексические) аксессоры.
PS Понятное дело, что статичные, финальные, иммутабельные константы — это отдельная тема, но они и не имеют никакого отношения к свойствам объектов
Не совсем:
Тип выражения
{ return 5; }--!(он же never-type).Другое дело, что never-type, формально, приводим к любому типу и, действительно,
также скомпилируется.
Ломбок редактирует не байткод.
Он работает на уровне сурсов (если точнее, то AST), используя внутренние API javac/ecj (публичное API не даёт редактировать дерево существующих сурсов, а только позволяет создавать новые).
Как раз то, что вы привели в примере, не будет валидно в случае мьютексов Раста.
Валидным тут будет:
Непосредственно на
dataвы никакие операции с данными выполнять не можете.Интересные вопросы.
Постараюсь ответить по пунктам:
Не совсем понимаю, какой вред носит реализация тех или иных трейтов библиотечным типом. Если некоторый тип T реализует трейт Foo, это влияет только дополняет то, что с ним можно делать, но никак -- по-моему -- не усложняет взаимодействие с ним. Если этот типаж Вам, как пользователю API, не нужен, то вы об этом даже и не задумываетесь. Если же он окажется нужным, то вам же будет плюсом то, что он реализован для данного типа.
Как по мне, в C++, как раз, с этим хуже, потому что многие решения (точнее, все), завязанные на активном использовании статического полиморфизма, полагаются сугубо на около-утиную типизацию (утрированно), а именно наличие функций/полей/.., "похожих" на то, что ожидается. Тут и не получается получать реальные статические гарантии того, что это именно то, что нужно (например, у типа могут быть
begin()иend()методы, но при этом он может не иметь никакого отношения к итераторам), и, при этом, нельзя реализовать полноценную совместимость между библиотеками, в случае, например, коллизий каких-либо имён (условно,libFooтребует метод::std::string name(), аlibBar--Id name(); пример искуственный но суть, думаю, передаёт).Более того, многие опциональные реализации трейтов (например, для поддержки популярных, но не обязательных библиотек) во многих проектах спрятаны за feature-флагами, так что, если они вам не нужны, вы и не будете их включать.
Довольно странно. Не буду бросаться утверждениями, но, как мне кажется, то, что вы видели, это либо те случаи, когда известно, что ошибка невозможна, но, в общем случае, возвразается
Result/Option(например,127.0.0.1"::parse::<IpAddr>()), либо вы натыкались на неидеоматично написанный код.Как раз, большинство очень активно пользуется этим и пишет API именно под такую модель работы с ошибками.
Более того, существует прекрасный try-operator --
?, который по смыслу (с некоторыми допущениями) превращаетв
За счёт этого, в большинстве случаев, проброс ошибки вверх по стеку -- это просто написание оператора-вопросика после нужного значения.
Паники, в целом, не предназначены для обработки ошибок бизнес-логики. Идеология
Result, которую уместно и в другие языки перетащить -- это то, что ожидаемые ошибки -- это такие же равносильные результаты вызова функции, как и успешные значения, и, при этом, взаимоисключающие и, соответственно, в большинстве случаев, нет смысла использовать для этого отдельный механизм.Утрируя, мы ведь не пишем код вроде (псевдокод):
Потому что как
true, так иfalseоба ожидаемые результаты вызова функции. Так и, для большинства сценариев, ошибочный результат -- это тоже результат, а не какой-то отдельный исключительный сценарий.Более того, этот подход ещё и дешевле, потому что не приходится поддерживать инфраструктуру, связанную с заполнением стек-трейса и прочими вещами, которые в большинстве случаев не нужны: с болью привожу как антипример многое из API джавы, где есть
Integer#parseInt(String), который преобразует строку к числу, но, если строка не число -- кидает исключение, хотя во многих сценариях разработчик ожидает, что пользователь введёт не число и ему нет смысла от полноценно созданногоNumberFormatExceptionс заполненным стек-стрейсом (что очень дорого), который он тут же хочет обработать:Аналогичный код на Расте:
И таких сценариев большинство.
Механизм же паник предназнчен именно для того, чтобы безопасно (точнее, корректно) сообщить о том, что какой-то относительно хлипкий инвариант был нарушен, но это не имеет какого-то отношения к логике программы и, скорее всего, не подразумевает дальнейшей обработки. Например, есть
std::unreachable!, который нужен для того, чтобы помечать недостижимый (по мнению разработчика) код, который, в случае, если до него таки дошло исполнение, паникует. Обрабатывать панику тут, в оббщем случае, нет никакого смысла, потому что её первопричина -- логическая ошибка в коде самого разработчика.Это такой аналог unchecked exceptions из той же Джавы.
Надеюсь, что смог ответить на волпросы :)
Так, ещё раз, а какой в этом смысл?
Зачем добавлять метод, который никогда не должен использоваться и не имеет смысла? Причём, более того, который даже не является примером для разработчика, как писать подобный, потому что:
его даже в общих чертах нельзя описать в
Enumразработчик итак никогда его не должен (и не сможет) реализовать, потому что вручную от
Enumнаследоваться нельзяВ итоге наличие такого метода вводило бы лишь в заблуждение.
Что значит "показать реализацию"?
В пределах
Enumего нельзя реализовать, потому что для него этот метод не имеет смысла. И его наличие там никакой пользы не принесёт, т.к. обращение непосредственно к нему - бессмысленно, а обращение к его тёзке в конкретном enum'е отношения к нему иметь никакого не будет.Но какой в этом смысл? Для самого
Enumего нельзя корректно определить (= он бессмысленен); а при "перекрытии" у нас просто будет идентичный по названию и сигнатуре, но, в общем, никак не связанный с предыдущим, другой метод.
Где именно?
В вызове unsafe-функции? - потому что это также unsafe действие.
В определении unsafe-функции? Потому что она ведёт к UB, если вызывающий не соблюл инвариант (неалиасность указателей).
Потому что существует реализация свапа для указателей, которая умеет делать это очень быстро при условии соблюдения инварианта. Этот инвариант гарантирован для
&mut T, а, значит, нет необходимости делать повторную реализацию для него, когда можно воспользоваться той.Почему же вы считаете, что это нельзя выразить формально на Расте?
От чего же нельзя? Можно:
Но это та самая наивная и неэффективная реализация, от которой мы старались уйти. Причём, ограничение в виде реализации
Cloneприсутствует и в C++ версии, а именно требование на copy- и move-assignable (надеюсь, cppreference будет достаточно корректным источником тут).Библиотечный примитив мог быть реализован и так, но (и это уже детали реализации) была возможность сделать это, используя unsafe.
Плохо ли это? По-моему, нет, потому что это никак не влияет на пользователя безопасной функции
swap. Он наоборот получает в составе стандартной библиотеки необходимый примитив и не должен сам заботить себя необходимостью где-то вручную проверять его корректность или, вообще, предпринимать какие-либо действия для его работоспособности.Что же касается тезиса о том, что стандартная библиотека написана с большим количеством unsafe, то в чём именно проблема этого? В Расте есть unsafe и никто этого не отрицает, но идея в том, что в клиентском коде его прекрасно получается избегать.
Иначе (далее утрированный пример) можно точно так же сказать, что почему-то выразить средствами C++ (практически любого ЯП, на самом деле) сложение двух целых чисел можно, но это будет очень медленно (цикл (который, как и случай со swap, может сам требовать некоторый более примитивный аналог самого себя, в данном случае для счётчика цикла)) либо небезопасно (машинно-специфичный АСМ с учётом размерности, эндианности etc, причём это ещё если данный ЯП такой инструмент даёт). А то, что для этого есть "особый оператор +" считать признаком слабости языка.
Или же говорить, что использование инлайн-АСМа это признак слабости языка.
Очень даже используются, использование двух mut-ссылок гарантирует, что они не алиасятся, что и является соблюдением инварианта unsafe-вызова для полученных из них указателей, которое (соблюдение неалиасности ссылок) в свою очередь гарантирует нам компилятор.
Почему же нет никакого смысла? Это хинт компилятору (не важно, какой там бэкенд, LLVM, GCC, whatever), что данная ветка гарантированно (разработчиком) недостижима, и компилятор (снова же, я не ссылаюсь на какой-то конкретный, а говорю, в общем, что значит эта (псевдо)функция) имеет право рассматривать это, как истину в последней инстанции. Несоблюдение этого инварианта есть UB, и не потому, что "так где-то сделано", а потому что вся суть этой функции в том, чтобы дать строгую статически-недоказуемую гарантию. Отсутствие unsafe (то есть, UB при несоблюдении инварианта) значило бы, что даже если ветка была бы достигнута, она должна была бы как-то конкретно быть обработана, для чего и существует не-unsafe аналог, который паникует (можно провести аналогию с плюсовым исключением) при достижении.
Конкретно в данном примере корректность условной деконструкции.
Конкретно конструкция if-let является общим инструментом сравнения с паттерном, а не каким-то частным случаем для
Option(не говорю, что Вы подобное утверждали, но, на всякий случай, здесь уточню это, чтобы не было дальнейшей полемики на эту тему).Причём, опять же, что плохого в том, что семантика примитивная? Она простая, и при этом позволяет чётко описать действие - это же (синтаксис) и входит в понятие инструментария языка, о котором шла речь изначально.
А что до бесплатности, то куда ему быть ещё бесплатнее? Это пара проверка+доставание, которая в том же C++ (да и, в целом, любом ЯП) одинаково дорогая. Причём тут, поскольку это часть синтаксиса языка, очевидно, что это одно из первых мест, которые оптимизируются во всю (относительно, условно, каких-то конкретных функций).
А что именно тут не так? Это и не type refinement, формально, а просто pattern matching. Хотя играет он тут ту же роль: ограничивает в пределах блока
ifмножество значенийvalueтипомT(сахара ради, новая переменная внутри if-let может, иметь точно такое же название, как и изначальная переменная, а ляif let Some(value) = value).В том то и дело, что в C++ в абсолютно изначально корректном месте приходится использовать unsafe (иначе говоря, UB-провоцирующую) функцию тогда, когда она таковой не является. Вся причина в том, что нету той самой пары проверь+возьми одним "атомарным" выражением, и система типов действительно делает тот UB синтаксиески возможным.
Вы совершенно верно подметили ниже функциональный вариант с
map, который не пользуется спросом. Функциональный вариант тут и менее читаем (в некоторых случаях, хотя бы из-за громоздкости синтаксиса лямбд) и менее удобен (банальный capturing ставит палки в колёса) да и, полагаю, не очень идиоматичен.Вы выше, если не ошибаюсь, писали, что Раст не даёт ничего для безопасности с нулевой ценой, но ведь тут именно оно - операция, которая в C++ описывалась парой никак не связанных statement'ов, тут представлена одним, при этом не оставляя места даже для формального UB.
Суть паник именно в том и состоит, чтобы сообщить о том, что какой-то хрупкий (=недоказуемый статически) инвариант был нарушен, не провоцируя при этом UB, но использование их для какой-либо бизнес-логики это явный антипаттерн. Ровно как в C++ не стали бы (я полагаю) ловить какой-нибудь
std::invalid_argument.Тот же самый
unwrap, как раз, не столь "идиоматичный" (в виду наличия всё того же if-let, match и кучи функциональных методов), но позволяет, с одной стороны, гарантированно со стороны разработчика, но с другой - без UB (а с паникой в случае его ошибки) описывать случай, когда он знает, что значение есть, хоть и представлено вOption(например, когда это некоторый трейт, который возвращает, в общем случае, опциональное значение, но для какого-то конкретного типа оно всегда присутствует, причём с окончательным прибытие never-type (!) многие места, где нынеunwrap, будут прекрасно безусловно (let вместо if-let) раскрываться для, к примеру,Result).---
Что же касается того уточнения, что при помощи
unsafeможно получить два алиасищихся&mut T, то это и есть то самое UB, которое должен избегать соблюдением инварианта разработчик, и если вunsafe-блоке некто сделал бяку и вернул в результате два некорректных mut-указателя (в данном случае, указывающих на пересекающуюся зону) то это и есть UB, и гарантий, когда именно оно выстрелит, нельзя дать. Иначе говоря, наша программа уже, формально, в некорректном состоянии, а не вызовswapявляется небезопасным.Не очень понимаю такой зацикленности на моей фразе про идею, учитывая то, что изначально дискуссия была о более прикладной составляющей (в моём понимании), а не том, кто первый что изобрёл (по крайней мере, в этой части обсуждения участвовать не хочется), но, хорошо, соглашусь, выразился неудачно: это не что-то, что изобрёл Раст, это подход, который существует давно, но конкретно в нём позволяет на (практически) одном и том же языке писать что safe, что unsafe код, и при этом safe подмножество не несёт за собой оверхеда, характерного для управляемых языков.
Доказывался другой тезис, в именно то, что производительная низкоуровневая реализация (
swap_nonoverlapping_oneреализован, в общем, подобно тому, что вы отправили) не мешает быть точно такому же эффективному безопасному интерфейсу.Если речь об
std::swap, то почему вы в этом и предыдущем сообщении говорите о том, что кто-то будет вызывать его из unsafe, если это safe функция? Ещё раз,swapделегирует к unsafe методуswap_nonoverlapping_one, который требует соблюдения строго инварианта, но пользователя это не волнует. Потому что на вызовеswapэтот инвариант итак гарантирован. Задача реализацииswap- вызватьswap_nonoverlapping_one, потому что два алиасных&mut Tбыть не может, а вот для*mut Tтакие гарантии должен (для данной задачи) гарантировать разработчик. Но не разработчик клиентского кода, а разработчик реализацииswap. Я же как разработчик просто пользуюсьswapиз safe и не задумываюсь (утрированно) над тем, как он реализован, потому что я знаю, что вызывать его неправильно я чисто физически не могу.Полагаю, проблема в моей опечатке выше, речь шла о
has_value()(как могло быть очевидно из того, что я упомянул его вместе сoperator bool).Переформулирую: речь шла о том, что в случае Раста разработчик не может вне unsafe контекста достать опциональное значение просто сказав, что он обещает. В коде на плюсах проблема в том, что проверка на наличие значение и его доставание могут быть как угодно размазаны по коду и в дальнейшем проверка может быть просто забыта (да и даже при написании разработчик мог её забыть). Инструментарий плюсов никак этому не помешает, кроме, быть может, линтеров, но реальной гарантии корректности у вас нет. В Расте же у
Optionоперация проверки и доставания объединена, и вы просто никак синтаксически не можете выразить то, что вы достали без гарантии. Представленная вышеif let- это один, из множества вариантов это сделать, но каждый из этих вариантов защищён от небезопасного разыменования тем, что его просто нельзя осуществить. Да, если у вас реально есть какая-то причина полагать, что разыменования возможно без проверки, то вы можете положиться сделать его без проверки, но тогда будьте добры взять ответственность на себя, сделав это через ужеunsafeфункцию (насколько я помню, таковая реализована даже как отдельный крейт), потому что тут вы берёте на себя ответственность за то, что значение есть. Причём, скорее всего, если у вас есть причины полагать, что в опциональном значении значение есть всегда, то может и вовсе не стоит тут использоватьOption, а уместнее что-то другое? Ну а если это действительно экзотический случай и тут нужно так сделать, то тут уж пишитеunsafe, говоря, что, да, ответственность на вас.Иначе с чего вдруг компилятору (да и просто тому, кто работает с вашим кодом) быть уверенным, что разыменования tagged unionа без проверки типа корректно?
Как раз идея Раста в том, чтобы предоставить дешёвые (в идеале, с нулевой стоимостью) абстракции, которые при этом безопасны.
Достигается это именно путём того, что на низком уровне что-то (и то, относительно узкая часть, а именно "самые-самые" примитивы + интеграции с внешними функциями) реализовано с unsafe, однако предоставляемый интерфейс (в широком смысле) - safe.
Вот простой, но, как мне кажется, достаточно характерный пример, реализация
swapв стандартной библиотеке:Небезопасный
ptr::swap_nonoverlapping_oneнебезопасен по той причине, что требует, потому что (вероятно) производит разыменование сырых указателей, а, главное, требует того, чтобы эти области памяти, на которые они указывают, не пересекались. В то же самое время, borrowing checker не позволяет иметь два&mut T, ссылающихся на одну и ту же область, поэтому вызов небезопасногоptr::swap_nonoverlapping_oneот них безопасен. Что же до эффективности, то никакого оверхеда здесь нет. Преобразование ссылки к указателю это классический no-op. Инлайн же этой функции точно так же тривиален и почти наверняка (формально, компилятор имеет право решить, что он не нужен, но тут вероятность подобного стремится к нулю) произойдёт.Таким образом мы получили безопасный swap при этом обладающий всеми преимуществами, с точки зрения производительности, безопасного варианта. При этом разработчику, пользующемуся им, не важно, как он устроен под капотом, и при этом он никак не может вызывать эту функцию неправильно.
Идея unsafe не в том, что им помечено всё быстрое и волшебное. Unsafe является то, что ведёт к неопределённому поведению при нарушении инварианта, который компилятор не имеет возможности гарантировать.
Другой неплохой пример -
std::hint::unreachable_unchecked: он небезопасен, потому что переносит ответственность на доказательство того, что данный блок недостижим, на разработчика, зато даёт право компилятору полагаться на то, что этот блок гарантированно недостижим, например (пусть функцияrand(n)даёт случайное число от 1 до n):Компилятор не имеет никакой возможности явно доказать, что числа кроме 0 и 1 невозможны, потому что это логика вашей
rand, но вы можете сделать это, взяв ответственность за ошибку в случае, если это не так, на себя, причём строго в пределах блока unsafe. В то же самое время, если вы "не, уверены" есть и не-unsafe аналог который вставляет код для паники в место своего использования, хоть он и не даёт компилятору просто так вырезать эту ветку. Причём, в действительности, компилятор, быть может, и в состоянии сам понять, чтоrand(2)не может вернуть ничего, кроме 0 и 1 и в таком случае и вариант с паникой без проблем будет вырезан.Ну и другой пример для того, чтобы показать, что то, что в Расте safe, в целом, сделано наиболее дёшево, и аналогичный код на (например) C++ был бы ни чуть не быстрее: великий и могучий
Option<T>. Самый близкий аналог на плюсах -std::optional<T>. И классическая же операция дереференса. Что в расте, что в плюсах, если вы собираетесь достать из него T, вам необходимо проверить, есть ли он там вообще. В C++ для этого используется`operator bool()`илиvalue(), который можно банально забыть написать или при рефакторинге забыть изменить. Иначе говоря, проблема подхода там - неатомарность операций:Неатомарность в том значении, что компилятор не сможет никак гарантированно вам помочь, если вы забудете выполнить эту проверку. Может (i.e расширения), но не обязан, потому что с точки зрения языка код без проверки всё так же корректно. А плохо это потому что "быстрый"
operator *ведёт к UB, если значение отсутствует.Раст же даёт инструментарий (причём, довольно разнообразный), при котором чисто аналогичный код атомарен:
Как видно, код на обоих языках делает абсолютно одно и то же, но то, что в плюсах таит UB, которое может быть никем на замечено, в Расте обёрнуто в безопасную обёртку, которая при этом ничуть не проигрывает по производительности.
Ещё один момент:
На самом деле,
fork()отправляя задачу в поток на выполнение (пусть и без гарантии того, когда именно он ей займётся) и вызовjoin()не обязателен для того, чтобы она была завершена, что можно увидеть на следующем синтетическом примере:Последняя строчка успешно вызывается, что было бы невозможно, если бы задача не выполнилась.
Хорошая статья с точки зрения простоты объяснения.
Небольшое уточнение:
В данном случае, да, но есть уточнение на счёт случая вызова из потока не относящегося ни к какому FJP:
Если смотреть в Джавадок метода fork(), то мы увидим, что он по возможности будет использовать текущий пул (пул, к которому "относится" вызывающий поток), но в противном случае будет использовать глобальный. Например, поведение с
commonPoolможно было бы понаблюдать в Вашем примере с вызовомfork()напрямую изmain.Мелочь, просто чтобы не оставлять неочевидных моментов)
Ну почему же? Всё тот же Spring умеет в инициализацию через конструктор, либо, что ещё более приятно, static factory method. Единственное, что, по-моему, может мешать этому — циклические зависимости, но, это как раз чаще признак того, что что-то не так.
Неициализированные поля, по-моему, в целом проблема дизайна, потому что всё подобное (поля) стоит делать
final, инициализируя при создании объекта. Экзотику с не очень удачным DI, вроде оного в JFX, стоит рассматривать, скорее, как неудачное решение.Для локальных переменных они, как раз, не нужны, потому что там nullability однозначно выводится.
Тут, увы, да, но в пределах API, скорее,
nullа будут избегать (снова же, при удачном дизайне). Классический пример, пустая коллекция лучшеnullовой.Скорее уж тогда CLion:
Но, по сути, да, особого смысла в отдельной IDE сейчас нет.
Так у него есть, как таковая фича, extension methods.
Более того, в интелидже уже есть их минимальная поддержка.
Убрали реализацию конкретного движка из состава JDK.
Сам Scripting API реализуется много чем и интегрируется именно через javax.script
Из нынешних решений только два пути:
Unsafe#defineAnonymousClass(..)— способ, для которого, очевидно, тем более никто не даёт гарантий работоспособности и безопасности, но который более подходит для варианта, когда нужно загружать класс и отгружать, по возможности, как только у него нет экземпляров. Как пример, он используется для сгенерированныйLambdaMetafactoryиStringConcatFactoryклассов лямбд и соединителей строк, соответственно (актуально для текущей OpenJDK), а также в Nashorn для загрузки скомпилированных скриптов.На моём опыте, первый случай, как и было сказано, уместен для случаев, когда группа классов загружается и может быть в каком-то случае отгружена (eg всё те же плагины), а второй — для динамически генерируемых классов с жизненным циклом таким же, как и жизненный цикл их экземпляров (eg, я в одном из проектов для генерации динамических строк, на основе шаблонов, в рантайме генерирую класс с агрессивным инлайнингом, который, однако, нужен ровно столько, сколько существует его экземпляр).
Порой приходится, если описание конфигурации плагина, необходимой для его загрузки, записано, допустим, в аннотациях в некоторых классах плагина, однако при этом нужно избегать их загрузки.
Например, некая аннотация, указываются на то, какую зависимость нужно загрузить до данной или, например, просто указывающая на entry-point плагина.
По-моему, очень странное мнение у Ваших коллег.
На то несколько причин:
foo.bar = baz;какой-нибудьif (baz != null && foo.qux/* аналогично bad practice */).willNotBreakWith(baz))).Возможно, конечно, они говорили о свойствах объектов, вроде того, как Kotlin даёт доступ к ним через
foo.bar, а неfoo.bar(), однако они, в свою очередь, также реализованы через (синтексические) аксессоры.PS Понятное дело, что статичные, финальные, иммутабельные константы — это отдельная тема, но они и не имеют никакого отношения к свойствам объектов