Первый стабильный релиз Rust появился в 2015 году, и каждый год, начиная с 2016, он признаётся в Stack Overflow’s Annual Developer Survey самым любимым языком (в 2023 году эта категория называется «обожаемый»). Почему же разработчики, ощутившие вкус Rust, не могут отказаться от его использования? Похоже, в мире прогремевших наследников C/C++ репутация растёт только у Rust. Как же этот язык, появившийся на сцене меньше десятка лет назад, стал настолько популярным?
Ржавый красный краб Феррис по версии Midjourney
Кривая обучения оказалась крутой. Я нашёл многое, что мне нравится в Rust, но постоянно попадал в его ловушки. Однако в конечном счёте именно препятствия и проблемы, с которыми столкнулся, я научился любить больше всего.
Я начну историю с разговора о том, что легко полюбить — со среды Rust, управления пакетами и документации. Затем я расскажу о системе типов и типажах (trait). Далее я поведаю о тех возможностях тестирования и test driven development, которые становятся возможными благодаря Rust. Наконец, мы обсудим самую запутанную и сбивающую с толку часть — одержимость Rust тем, кто какой переменной владеет.
▍ Экосистема Rust
В большинстве регулярно используемых мной языков есть управление пакетами и версиями. В наши дни очень полезны системы наподобие npm, pip и NuGet, но они не всегда были такими, и всё ещё далеки от идеала. В большинстве языков само управление установленной версией языка становится мучением.
Установить Rust можно при помощи
rustup
— инструмента, который позже позволяет управлять версией Rust и связанными с ним инструментами.Cargo сочетает в себе функциональность управления пакетами и инструментов сборки; он воплощает все лучшие характеристики управления пакетами. Он прост и никогда не мешает работе.
Ещё один важнейший аспект экосистемы Rust — это документация. Я изучал язык исключительно по официальной документации и у меня никогда не было потребности искать туториалы в других местах. В «книге» и в Rust By Example было раскрыто всё необходимое мне. На самом деле, когда бы я ни приходил на Stack Overflow с вопросом, самые полезные ответы обычно заключались в указании на подходящий раздел или в официальной документации, или в одном из этих двух источников.
Я могу долго рассуждать о сообщениях компилятора, которые ощущаются как менторство со стороны более опытного программиста (это я оставлю на потом), или о Rust Playground — прекрасном способе проверить, работает ли код. Но вместо этого я перейду к действительно выделяющимся особенностям языка. Настало время углубиться в тонкости системы типов Rust, и в особенности в концепцию типажей (Trait).
▍ Кря-кря! Утиная типизация с типажами
В самом начале у Rust были классы, но они продержались меньше полугода. Их заменила гораздо более простая структура данных —
struct
. Типы определяются объявлением struct
, которая является чем-то большим, чем набор связанных полей для хранения данных. Rust позволяет добавлять реализации для типов, которые являются множествами функций, способных выполнять операции, связанные с этими типами, или над ними.Удобная концепция, которую я полюбил при работе с языками с динамической типизацией — это утиная типизация. Это принцип, по которому функция может принимать объект любого типа, если он имеет подходящие свойства и методы, нужные функции. «Если это выглядит как утка, плавает как утка и крякает как утка, то это утка». Если вызываемой нами функции нужен ввод, способный плавать, то нас не должно волновать, утка ли это. Нам важно только, умеет ли это плавать.
Слон, притворяющийся уткой (Midjourney)
Несмотря на то, что Rust — язык со статической типизацией, он всё равно справляется с этим изящно. Благодаря типажам вы можете перестать думать, какие типы должны принимать ваши функции, и вместо этого начать думать о том, какие входные данные функции должны уметь обрабатывать.
Давайте взглянем на пример. Вот типаж для плавания. Любой тип, реализующий типаж
Swim
, умеет плавать.trait Swim {
fn swim(&self);
}
Функция, имеющая аргумент, который должен уметь плавать, не обязана указывать тип. Ей достаточно указать, что он должен реализовать типаж
Swim
. Нас не должно волновать, какие типы появятся в будущем. Компилятор будет смотреть, какие типы мы вызываем с функцией, выполнять соответствующие проверки и генерировать соответствующий машинный код для их обработки.fn cross_the_pond(animal: impl Swim) {
animal.swim();
}
Давайте создадим несколько типов, которые можно передавать функции
cross_the_pond
. Мы можем создать тип Duck
, определив struct
и реализовав для неё типаж Swim
.struct Duck {
name: String,
}
impl Swim for Duck {
fn swim(&self) {
println!("{} paddles furiously...", self.name);
}
}
Но утка — не единственное, что может плавать. Давайте определим
struct
Elephant
и тоже реализуем для неё типаж Swim
.struct Elephant {
name: String,
}
impl Swim for Elephant {
fn swim(&self) {
println!("{} is actually just walking on the bottom...", self.name);
}
}
Наша функция
main
способна создавать экземпляры уток и слонов, соединяя всё это вместе.fn main() {
let duck = Duck { name: String::from("Sir Quacks-a-lot") };
let ellie = Elephant { name: String::from("Ellie BigEndian") };
println!("Crossing the pond...");
cross_the_pond(duck);
cross_the_pond(ellie);
}
При этом получается следующий вывод:
Crossing the pond...
Sir Quacks-a-lot paddles furiously...
Ellie BigEndian is actually just walking on the bottom...
Можете поэкспериментировать с этим кодом в Rust Playground здесь.
Кроме того, в стандартной библиотеке Rust есть очень полезные типы наподобие
Option
и Result
, позволяющие обрабатывать случаи, в которых значение может существовать, а может и не существовать. Благодаря сопоставлению с образцом (pattern matching) Rust при помощи этих типов можно писать сжатый и читаемый код обработки ошибок. В этой статье мы не будем рассматривать их или оператор match
, но если вы только осваиваете Rust, с ними стоит ознакомиться. Вместо этого давайте поговорим о подходе Rust к тестированию.▍ Тестирование кода в коде
У разработчиков обычно есть устоявшиеся мнения о структуре папок и формате именования файлов. Все согласятся, что мы стремимся поддерживать максимальную чистоту в папках, но люди обычно расходятся в том, что же это на самом деле значит. Важная причина споров заключается в том, куда помещать тесты. Нужно ли создавать для них отдельную папку? Должна ли структура папки с тестами копировать структуру папки с исходниками? Добавлять ли к файлам тестов префикс «test_», чтобы они были сгруппированы вместе, или добавлять суффикс "_test", чтобы тесты были рядом с тем, что они тестируют?
Запутанная структура усложняет поиск. Но какой должна быть аккуратная структура? (Midjourney)
Ещё одна проблема — это тестирование приватных функций. В большинстве языков у вас есть выбор: или остановиться на тестировании только публичных интерфейсов, или сделать приватные функции публичными (это отвратительно, пожалуйста, не делайте так), или положиться на трюки с рефлексией, из-за которых тесты становятся неуклюжими и сложными в чтении и поддержке. Как с этими трудностями справляется Rust?
В Rust рекомендуется помещать тесты в тот же файл, что и код, который они тестируют. Преимущества этого огромны. Отсутствует хаос в файловой системе, споры о способах именования, а сами тесты при необходимости могут иметь доступ к приватным функциям без необходимости компрометации или раскрытия подробностей реализации.
Давайте рассмотрим пример. Показанный ниже модуль обеспечивает одну глупую функцию, складывающую два числа и возвращающую удвоенную сумму. Она использует приватную вспомогательную функцию и несколько тестов.
// Публичная функция, получающая два integer и возвращающая их удвоенную сумму
pub fn add_and_double(x: i32, y: i32) -> i32 {
2 * _add(x, y)
}
// Приватная вспомогательная функция, складывающая два integer
fn _add(x: i32, y: i32) -> i32 {
x + y
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_and_double() {
assert_eq!(add_and_double(2, 3), 10);
assert_eq!(add_and_double(0, 0), 0);
}
#[test]
fn test_add() {
assert_eq!(_add(2, 3), 5);
assert_eq!(_add(0, 0), 0);
}
}
Атрибут
#[cfg(test)]
приказывает компилятору компилировать тестовый модуль только при выполнении тестов, а из сборки продакшена тесты вырезаются.В Rust также имеются очень удобные функции наподобие тестов документации (примеры в документации выполняются в качестве тестов, чтобы документация никогда не теряла актуальности) и конкурентное исполнение тестов, делающее выполнение тестов крайне быстрым.
Типизация и тестирование были теми двумя особенностями, которые сразу же восхитили меня, но давайте рассмотрим и особенность, которую полюбить сложнее всего.
▍ Позаимствовал и теперь не отдаёт
Для меня самой сложной частью изучения Rust стало понимание концепции владения (ownership), времени жизни (lifetime), заимствования (borrowing), перемещения (moving) и копирования (copying).
Rust — это язык, проектируемый как безопасный с точки зрения доступа к памяти по своей сути. Источником вдохновения для создания Rust стал сбой в операционной системе лифта в жилом здании. Сбои часто вызывают ошибки в памяти, в том числе нулевые указатели, висячие указатели и утечки памяти, которых можно избегать, создавая более качественные программы. Система владения Rust гарантирует отсутствие подобных ошибок.
Значение в памяти имеет лишь одного владельца. Владелец — это только переменная, содержащая значение, и компилятор может во время компиляции понять, когда владелец вышел за границы блока (scope), и точно знает, когда можно освободить память. Весь другой блок, которому нужно использовать значение, должен заимствовать его, и за один раз значение заимствовать может только один блок. Это гарантирует, что одновременно не может быть больше одной ссылки на значение. Также в языке есть строгая система управления временем жизни, поэтому ссылка на значение не может жить дольше, чем переменная, владеющая значением. Эти концепции являются фундаментом безопасности доступа к памяти Rust.
Если вы пишете код логичным и удобным образом, ваши функции должны быть короткими, а переменные не должны жить слишком долго. Но у любой полезной программы есть необходимость хранить данные намного дольше, чем один вызов функции. На сцене появляется перемещение (moving).
При возврате значения из функции или при присвоении его новой переменной другая переменная должна получить владение этим значением. Это называется перемещением; после перемещения исходной переменной больше не разрешается использовать значение. Это значит, что показанный ниже простой код, который не был бы проблемой в большинстве других высокоуровневых языков, в Rust не скомпилируется.
fn main() {
let original_owner = String::from("Something");
let new_owner = original_owner;
println!("{}", original_owner);
}
Меня это очень сбивало с толку. Давайте взглянем на сообщение об ошибке:
error[E0382]: borrow of moved value: `original_owner`
--> src/main.rs:6:20
|
3 | let original_owner = String::from("Something");
| -------------- move occurs because `original_owner` has type `String`, which does not implement the `Copy` trait
4 | let new_owner = original_owner;
| -------------- value moved here
5 |
6 | println!("{}", original_owner);
| ^^^^^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
4 | let new_owner = original_owner.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
Это типичная структура сообщений об ошибках от компилятора Rust. Она не орёт на вас, не давая понять, в чём же вы ошиблись, а спокойно указывает, почему значение необходимо переместить, показывает, где произошло перемещение, подсказывает, как это можно исправить и даёт полезное уведомление о снижении производительности при обходном решении.
Задумайтесь об этом. Когда я в первый раз встретился с умеренно сложной ошибкой компиляции, вызванной перемещённым значением, мне казалось, что я бьюсь головой в стену. Нечто столь простое, что я всю свою карьеру программиста считал само собой разумеющимся, было в новом языке невозможно. Почему же так много разработчиков любят этот язык, делающий простые вещи столь раздражающими?
Но потом я понял. Rust позволяет мне делать то, что я хочу, но сначала просит подумать, действительно ли я хочу именно этого, и заставляет задуматься о последствиях моего решения. Теперь когда я пишу код на других языках, то думаю о том, какие объекты хранят ссылки на какое значение, где на значения ссылаются или где их копируют, а также о влиянии этого на производительность и надёжность. Rust делает меня более совершенным программистом, даже когда я пишу код на других языках.
▍ В заключение
В 2019 году я выступил с докладом «Не только синтаксис» о своём опыте изучения Racket — языка из семейства Lisp. Хотя я никогда не пользовался и не буду пользоваться языком Lisp профессионально, этот опыт привёл к глубокому прозрению относительно функционального программирования на уровне, которого я не ощущал ранее. Завершил доклад я следующими цитатами:
«Язык, который не влияет на то, как вы думаете о программировании, не стоит освоения», — Алан Перлис
и
«Скрипка формирует не только скрипача, всех нас тоже формируют используемые нами инструменты, и в этом языки программирования имеют косвенное влияние: они формируют наши мыслительные привычки», — Эдсгер Дейкстра
Откровения, снизошедшие на меня при изучении Rust в течение последних нескольких месяцев, трансформировали меня схожим образом. Кривая обучения была крутой. Rust — не нежный и не прощающий язык. Он строгий и чёткий, но это лишь мешает вам писать код, о котором вы пожалеете спустя пару лет.
Я обнаружил то, что видят в Rust почти 85% использовавших его разработчиков, и когда на мою электронную почту пришла форма с опросом Stack Overflow на 2024 год, в которой был вопрос, буду ли я пользоваться Rust в следующем году, ответом было твёрдое «да».
Узнавайте о новых акциях и промокодах первыми из нашего Telegram-канала ????
Комментарии (139)
GospodinKolhoznik
25.10.2023 13:35+13Вакансий на Rust очень мало, а желающих писать на нём очень много. Поэтому для интереса потыкать можно, но выбирать его своей профессией так себе идея.
codecity
25.10.2023 13:35+11Поэтому для интереса потыкать можно, но выбирать его своей профессией...
Ну так потому его и любят - когда чем-то занимаешься для души в свободном ритме - это в кайф. А когда это становится обязаловкой и нужно выдавать объем работ - вот тогда любовь и проходит. Для души можно и C++ любить, если нет насилия в виде требований выдавать объем работ.
В Rust свои минусы тоже есть - как то макросы, которые довольно сложны в отладке. Отсутствие полноценного ООП во многих сценариях - не удобно. Но обо всем этом начинаешь задумываться, когда нужно выдавать объем работы.
DarkEld3r
25.10.2023 13:35+3Пять лет пишу на расте за деньги - отвращения пока не появилось. Да, не всё идеально и (хочется думать, что) я не фанатик языка - если появится что-то удобнее и лучше, то готов переключиться, но С++ даже последней версии (для меня) таковым языком не является.
Но да, было бы здорово, если бы на расте было побольше не блокчейновых проектов.
Apoheliy
25.10.2023 13:35+1let new_owner = original_owner;
В C++ был подобный заход через auto_ptr. По-моему, скоро стало ясно, что нужно делать либо все владеют одной копией (shared_ptr), либо делать явную операцию перемещения.
Также из статьи не очень понятно, почему rust противопоставляется c++. Вот прямо подставь вместо c++ python (или другой язык) - статья особо не поменяется. И тогда всё сводится к тому, что есть
любимая женалюбимый язык, и нелюбимый.codecity
25.10.2023 13:35+6В C++ был подобный заход через auto_ptr.
auto_ptr уже все, полномочия закончились. Теперь - unique_ptr. Но только с unique_ptr C++ не контролирует владение, что чревато ошибками. Вот вам пример, который минимально демонстрирует проблему отсутствия контроля владения:
#include <iostream> struct Class1 { const int i1 = 42; }; void fun1(std::unique_ptr<Class1> smart_ptr){} int main() { auto smart_ptr1 = std::make_unique<Class1>(); fun1(std::move(smart_ptr1)); std::cout << smart_ptr1->i1 << std::endl; }
Rust - выдал бы ошибку компиляции в строке 12.
Apoheliy
25.10.2023 13:35+1Чисто для моего общего развития: что скажет компилятор, если сам вызов fun1 будет под if c некоторым условием, значение которого на момент компиляции неизвестно? Или это другое?
codecity
25.10.2023 13:35+5если сам вызов fun1 будет под if c некоторым условием, значение которого на момент компиляции неизвестно
Тут. Даже если условие известно и не выполняется - все равно не пропускает компилятор.
Походу он тупо проходит сверху вниз и смотрит вызовы, не взирая на условия. Как бы перестраховка. Если закомментировать 11 строку - то скомпилирует.
cdriper
25.10.2023 13:35что лишний раз говорит о том, что не надо использовать явный move
там где надо, он всегда работает автоматически и безопасно
mayorovp
25.10.2023 13:35+4Ну да, ну да, полностью автоматически...
{ auto smart_ptr1 = std::make_unique<Class1>(); fun1(smart_ptr1); // Копирование либо ошибка компиляции }
Казалось бы, очевидно же что переменная smart_ptr1 более нигде не используется, и её следует переместить. Но нет, без явного std::move фиг вам, а не перемещение!
cdriper
25.10.2023 13:35+1ну так сегодня у вас func1() последняя строчка в блоке, а завтра вы туда что-то еще допишете и начнете использовать smart_ptr1...
очевидно, что желаемое должно выражаться однозначно, а не основании того, что там ниже по течению у вас происходит
однозначный корректный код: fun1( std::make_unique<Class1>() )
mayorovp
25.10.2023 13:35+2очевидно, что желаемое должно выражаться однозначно, а не основании того, что там ниже по течению у вас происходит
Вы же только что предлагали автоматически и безопасно?
однозначный корректный код: fun1( std::make_unique<Class1>() )
…не работает если над переменной нужно сделать что-то ещё перед перемещением.
AxeFizik
25.10.2023 13:35+13ну так сегодня у вас func1() последняя строчка в блоке, а завтра вы туда что-то еще допишете и начнете использовать smart_ptr1...
Ну так компилирую я сегодня. Если завтра я что-то допишу, то пусть завтра компилятор и уберёт автоматический move.
AxeFizik
25.10.2023 13:35+4Блин, написал фигню и собрал плюсов ????♂️
Я подумал ещё раз и понял, что не хочу чтобы у меня неявно менялось поведение передачи аргументов в функцию, в зависимости от того, сколько раз вызывается эта функция.
k-morozov
25.10.2023 13:35разве флагами компилятора такого эффекта в С++ не добиться?
domix32
25.10.2023 13:35+1Нет. У компиляторов C++ нет полноценного статического анализа аналогичного borrow checker в Rust. Можно поставить сторонние (cppcheck, pvs studio, sonar qube и тп), но и без того сложный плюсовый пайплайн ещё более усложнится и это не считая того что время на сборку тоже вырастет. В имплементациях stdlib 23 стандарта там конечно улучшают кое какие вещи при помощи концептов, включая сообщения об ошибках, но это не решает language level проблемы.
Gorthauer87
25.10.2023 13:35+4Ну как сказать, одно дело, когда доступ к перемещенному значению запрещает компилятор и другое, когда проверка происходит только в рантайме. Да, молчу ещё про то, что в плюсах этот заход работает только с памятью в куче. Так что прекрасно понимаю, почему auto_ptr убили.
MountainGoat
25.10.2023 13:35+2Rust противопоставляется другим языкам, которые компилируются прямо в бинарный код и не имеют garbage collector. А это С, С++ и ... ? Только на таких языках можно написать прошивку контроллера, модуль для ядра Linux, серьёзный вирус. Если ограничивать область зрения микросервисами на средненагруженных серверах, то можно и одним PHP обойтись.
dpytaylo
25.10.2023 13:35По ощущениям, он противопоставляется даже большему количеству языков, таких как Java, C#. Наверное, из-за его довольно уникального подхода работы с памятью и неплохой продуманности языка (когда создавали, пытались избегать проблем, которые возникли в других языках). А так, можете хоть fullstack приложение на Rust написать, противопоставляя это fullstack приложению на JavaScript, ничего этому не будет мешать)
Sabirman
25.10.2023 13:35На си и c++ можно использовать динамические библиотеки с функциями. На java динамически можно подгружать библиотеки классов. Можно ли динамически подгружать библиотеки классов в rust ?
Gorthauer87
25.10.2023 13:35+4Можно, но есть свои грабли, нет стандарта на ABI, так что делается это на свой страх и риск. (Впрочем в плюсах тоже нет стандарта на ABI, но его уже лет 12 не меняли).
slonopotamus
25.10.2023 13:35+2но его уже лет 12 не меняли
А вы историю с _GLIBCXX_USE_CXX11_ABI к какому году относите?
Gorthauer87
25.10.2023 13:35Так это вроде где-то рубеж 10ого и 11ого годов, вроде
slonopotamus
25.10.2023 13:35Нет. Это переход с gcc-4 на gcc-5. Началось в 2015 году, но в куче дистрибутивов задержалось на несколько лет. А некоторые вообще только в прошлом году на новый ABI перешли.
Fell-x27
25.10.2023 13:35+2На нем и писать C-like либы можно. Я на Rust писал либу, которую цепляю к пхп через FFI - полет отличный.
Разве что на стыке с "внешним миром" приходится уходить в unsafe, но тут ничего не поделаешь. Специфика-с. Ну и не забыть вернуть указатель обратно в раст из пхп на убой, конечно, а то при вытаскивании наружу растовая магия перестает работать и можно на ура наплодить утечек.
LifeKILLED
25.10.2023 13:35+2Спасибо за статью. Очень ëмко и просто навела на некоторые мысли.
Я не мог начать пользоваться Rust пять лет назад из-за того, что просто не представлял, как со всеми этими ограничениями можно что-то сделать. То есть нужно действительно начать мыслить по-другому.
Сейчас у меня есть опыт параллельного программирования в С++, в этом очень сильно помогли умные указатели, которые следят за владением объекта и чисткой памяти. Они гарантируют отсутствие битых указателей, т.к. пока какой-то поток "одолжил" указатель, память в нем не чистится. Как недостаток, нельзя сказать точно, когда в рантайме объект освободится и в какой момент запустится деструктор, а главное из какого потока (например, к OpenGL можно обращаться только из главного потока, поэтому просто так из деструктора объекта, хранящиеся в shared_ptr, данные OpenGL не удалишь).
Тем не менее умные указатели и ссылки в плюсах - очень крутые средства, спасающие от выстрела в ногу. Мне кажется, заставив себя на них перекатиться, я постепенно дойду и до понимания Rust.
А теперь вопрос (он наверняка гуглится, но если всë гуглить, то о чем писать в комментах?). Rust многие вещи делает в компиляторе. Это ведь и по эффективности получается лучше, чем ООП в плюсах со всякими контейнерами и умными указателями? В плюсах это наверняка сплошной рантайм и достаточно тяжелый.
DancingOnWater
25.10.2023 13:35Это не совсем корректный вопрос. Да, некоторые из тех проверок которые в Си++ проходится делать в рантайме, в Расте можно делать на этапе компиляции. Но вот в чем прикол: понятие "объект" у вас при этом исчезает. И если во многих случаях без него можно обойтись не потеряв производительности (хотя придется и поломать голову), то в некоторых областях (тот же пользовательский интерфейс) это доставлет массу проблем, которые, вполне возможно, выливаются в потерю производительсноти.
SpiderEkb
25.10.2023 13:35+2Писал на С, затем С++ (как основной язык) с ... 90-91-го и по 17-й годы (сейчас тоже иногда, но это уже "второй язык" для решения определенного класса задач). И никогда не было проблем с памятью (в т.ч. и в многопоточных приложениях). Просто есть правило, которое следует неукоснительно соблюдать (отступления возможны лишь в самых исключительных случаях и всегда должны быть явно выделены в коде (комментарии и т.п.). А именно - "кто девушку ужинает, тот ее и танцует". Т.е. за освобождение (и вообще контроль за состоянием) памяти отвечает тот, кто ее выделил. И никак иначе.
Rust многие вещи делает в компиляторе. Это ведь и по эффективности получается лучше, чем ООП в плюсах со всякими контейнерами и умными указателями? В плюсах это наверняка сплошной рантайм и достаточно тяжелый.
Да. Основной минус ООП (как минимум его плюсовой реализации) - в изрядных накладных расходах на создание и удалении объектов в рантайме.
По поводу накладных расходов я писал в комментариях к статье «Чистый» код, нет проблем с производительностью. Например, так все то же самое делается без ООП и лишних вызовов конструкторов, чисть на статической инициализации. Тут и тут показывал что поддержка определенных вещей на уровне языка (например, типов данных соответствующих типам данных БД) намного эффективнее чем использования ООП и создания новых типов через ООП механизмы.
Вообще, по описанию, те концепции, что заложены в Rust как альтернатива ООП лично мне импонируют. Но нужно вникать глубже - возможно и там есть подводные камни.
mayorovp
25.10.2023 13:35+3А именно - "кто девушку ужинает, тот ее и танцует". Т.е. за освобождение (и вообще контроль за состоянием) памяти отвечает тот, кто ее выделил. И никак иначе.
Увы, это слишком ограничивающий принцип, особенно в многопоточных программах...
SpiderEkb
25.10.2023 13:35Тем не менее, это работает и позволяет контролировать работу с памятью самому, а не полагаться на "времена жизни объекта" и вот это все вот (что еще больше ограничивает на самом деле).
Уж различной параллельной обработки в своей жизни написал преизрядно. Тут вопрос изначальной проработки архитектуры приложения.
PrinceKorwin
25.10.2023 13:35+2Этот подход, безусловно работает, но имеет свои пределы. Как следствием этого подхода - приходится больше данных передавать по значению, а не по ссылке. Это получается безопаснее, но вот производительность от этого страдает.
mayorovp
25.10.2023 13:35Плюс сложную структуру данных так просто по значению и не передать без выделения памяти.
SpiderEkb
25.10.2023 13:35Передавать данные по ссылке можно. Но контролировать память лучше в одном месте. От и до.
Или это должен быть контракт заранее оговоренный - "я тебе предаю выделенный мной блок памяти, который ты должен освободить когда он тебе станет не нужен". В целом допустимо.
Так или иначе, мое мнение в том, что память надежнее контролировать самому.
PrinceKorwin
25.10.2023 13:35+3Люди не идеальны, они делают ошибки. Иногда часто, иногда реже. И у меня больше доверия компилятору чем самому себе.
SpiderEkb
25.10.2023 13:35+5Вот к компилятору С++ с его огромным количеством UB как-то особого доверия нет...
PrinceKorwin
25.10.2023 13:35Поэтому мне и не нравится C++ :)
SpiderEkb
25.10.2023 13:35+1Мне тоже :-)
И потому, что вместо того, чтобы понижать количество UB, туда тащат все, что увидели в других языках - чтобы "у нас все это тоже было, но с вистом и профурсетками". В результате получается какой-то Франкенштейн, а разработчики компиляторов просто не успевают за стандартописцами. И подавляющая часть разработки идет на С++ "предыдущих поколений". Особенно, на "долгоиграющих" проектах, подразумевающих длительную поддержку и развитие.
KanuTaH
25.10.2023 13:35И потому, что вместо того, чтобы понижать количество UB, туда тащат все, что увидели в других языках
C++ так-то находится на передовой разработки новых концепций (особенно в области обобщенного программирования), так что скорее происходит несколько наоборот. Тот же RAII появился в C++, откуда он был перетащен в Rust и в некоторой степени в C# и Java (IDisposable, try-with-resources). Шаблоны - совершенно самобытная вещь потрясающей эффективности, находится в постоянном развитии - вариадики, свертки, автоматический вывод аргументов, вот это вот все. In place конструирование (
emplace()
и сотоварищи) из C++ в Rust так и не могут перетащить, хотя и были попытки - не хватает ряда механизмов, а в языках с GC его не может быть по определению. Ну и т.д.разработчики компиляторов просто не успевают за стандартописцами
Это одни и те же люди.
И подавляющая часть разработки идет на С++ "предыдущих поколений".
Понятие "предыдущего поколения" постоянно сдвигается.
SpiderEkb
25.10.2023 13:35+2Так что там с UB? Может быть для начала от них избавиться, а потом уже "пилить революционные фичи"?
Или ABI стандартизировать. Ну просто так, интереса ради.
Или все это скучно?
KanuTaH
25.10.2023 13:35+1Так процесс же открытый. Предлагайте пропозалы, покажите этим комитетским лохам, как надо. Может быть, тогда вы получше погрузитесь в тему и несколько разовьете свои представления о современном состоянии дел. А писать обличающие комментарии по Булгакову ("космического масштаба, ..." и т.д. по тексту) на хабрике много ума не надо.
CTheo
25.10.2023 13:35В Расте тоже немалый список официального UB: https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html
Viknet
25.10.2023 13:35+1Это как раз очень малый список.
Все они вызываются довольно прямолинейным нарушением инвариантов языка с помощью unsafe, и это является прямым следствием возможностей этого самого unsafe:unsafe
only means that avoiding undefined behavior is on the programmer
KanuTaH
25.10.2023 13:35Это как раз очень малый список.
Это неполный список (о чем, кстати, там и написано), а полный список неизвестен. Один из "приколов" растовского unsafe - при написании unsafe кода, ты никогда не можешь быть уверен, что то, что ты написал, не вызывает UB. Unsafe Rust куда "опаснее" в этом смысле, чем C или C++.
DancingOnWater
25.10.2023 13:35+1Unsafe Rust куда "опаснее" в этом смысле, чем C или C++.
Соглашусь со всем комменатрием кроме этого предложения. UB в С++, как и в Unsafe Rust контринтутивен и хрен редьки не слаще.
KanuTaH
25.10.2023 13:35UB в C++ хотя бы описаны - существует некая модель, в рамках которой существуют UB, и существует описание этой модели в виде стандарта. В Rust нет и этого (о чем, кстати, по ссылке тоже написано). (Контр)интуитивность - это уже другой вопрос.
qwerty19106
25.10.2023 13:35-2И это всё требует unsafe, который по определению может вызвать UB. Вот только в реальном коде unsafe почти никогда не нужен.
ksbes
25.10.2023 13:35Этот в принцип в более сложном проекте вырождается в написание своего велосипедного аллокатора со всякими счётчиками и сборщиком мусора.
Что на самом деле - плохая практика. Во-первых, нужно использовать уже готовый, отлаженный. А во-вторых, все преимущества ручного управления памятью теряются - тогда уж и язык надо менять.SpiderEkb
25.10.2023 13:35Ну... Может быть мои проекты был недостаточно сложны (всего-то порядка мегабайта кода), но проблем с ручным управлением памятью не испытывал. А производительность там была потребна на достаточно высоком уровне - "микроядро" системы мониторинга инженерного оборудования зданий - с одной стороны сеть промконтроллеров, с другой - несколько "интерфейсных клиентов". Микроядро выполняло роли монитора состояния контроллеров верхнего уровня, фильтра-маршрутизатора (что от кого кому передавать), реализовывало отношение "многие-ко многим" ну и еще много чего. Работало все это в режиме 24/7 (причем, "где-то там", куда физического доступа у меня не было, я мог только подключиться к ядру удаленно и смотреть что там происходит в реальном времени специальным клиентом-шпионом) в несколько потоков (как минимум - поток контроллеров, поток клиентов, поток обработки данных и поток мониторинга работоспособности (остальных потоков, сервера БД и т.п.). И ряд вещей там должен был выполняться с микросекундными таймаутами.
Никакого "своего аллокатора" там не было. Но архитектура всего этого выстраивалась очень тщательно.
Сейчас все намного проще. Вся параллельная обработка строится не на потоках, а на фоновых заданиях (запускаемых через spawn) с конвейером для раздачи данных от головы обработчикам. Ну и система совсем другая.
PrinceKorwin
25.10.2023 13:35Кстати язык Rust так и появился.
Сначала в Mozilla исп стандартный аллокатор. Потом стали писать свой. И в какой-то момент кодовая база этого аллокатора и проблем вокруг превысила предел и ребята решили, что проще написать свой новый язык, а не вот это вот всё :)
LifeKILLED
25.10.2023 13:35Очень актуально для игрового движка. При отрисовке каждого кадра приходится создавать и чистить большое количество данных (треугольники, иерархии игровых сущностей). Быстрее всего сразу "застолбить" побольше памяти, а потом с помощью собственного аллокатора брать оттуда кусочки под расчеты. Поскольку игра должна работать быстро и плавно, обращаться в ОС за памятью не эффективно, а данных перекладывать с места на место надо очень много
mayorovp
25.10.2023 13:35+1Передавать данные по ссылке можно. Но контролировать память лучше в одном месте. От и до.
Удачи вам контролировать память в одном месте от и до при использовании архитектуры "каналы + обработчики". А ведь она считается самой дуракоустойчивой в плане многопоточности, и не просто так...
Или это должен быть контракт заранее оговоренный - "я тебе предаю выделенный мной блок памяти, который ты должен освободить когда он тебе станет не нужен". В целом допустимо.
Ага, и самый простой способ "оговорить" такой контракт - использовать умный указатель с возможностью перемещения.
SpiderEkb
25.10.2023 13:35Удачи вам контролировать память в одном месте от и до при использовании архитектуры "каналы + обработчики". А ведь она считается самой дуракоустойчивой в плане многопоточности, и не просто так...
А как думаете была устроена архитектура многопотчного приложения, где один поток работал с UDP портом, получал оттуда данные, передавал их в поток обработки-маршрутизации, откуда они потом уходили в поток, работающий с TCP портом и там отправлялись дальше? Аналогично и наоборот - от TCP через обработку в UDP.
Все данные оформлялись в виде датаграмм.
mayorovp
25.10.2023 13:35И как же вы эти данные возвращали потом обратно, чтобы освободить память?
SpiderEkb
25.10.2023 13:35Там суть в том, что принятая с одной стороны датаграмма должна храниться до тех пор, пока не придет подтверждение ее получения с другой стороны. Так что получили - обработали - отправили - как пришло подтверждение - удалили.
Более того, по маршруту UDP->TCP одна посылка со стороны UDP могла уходить в несколько TCP каналов. А могла в один...
И все это годами работало (и все еще работает в нескольких местах, говорят, хоть я оттуда 6 лет как ушел) в режиме 24/7.
Никаких утечек памяти не было и нет. Просто делаем все аккуратно.
LifeKILLED
25.10.2023 13:35В моем случае это были ресурсы OpenGL, к которому из других потоков обращаться запрещено драйвером. А объекты могут удаляться и создаваться в разных потоках. Пришлось писать отдельную очередь, которая собирает графические ресурсы, а потом основной поток, владеющий контекстом OpenGL, все это обрабатывает, когда до него добирается
LifeKILLED
25.10.2023 13:35Действительно, после такого опыта многопоток кажется не сложным. Это и "один пишет, другие читают", и "как танцевать девушку". При этом мютексы ставились только на очень узкие места (в основном взятие или добавление объектов в контейнеры).
Но поскольку у нас игра, объекты может создавать и игрок, и поток подгрузки. Возникают очень хаотичные взаимодействия потоков в самых разных комбинациях. Каким-то образом всë это получилось разрулить и заставить работать быстро и без вылетов.
SpiderEkb
25.10.2023 13:35+9Как Rust меняет мышление разработчика
Любой инструмент меняет мышление того, кто им пользуется. Человек, хорошо владеющий инструментом, всегда будет рассматривать задачу с точки зрения того, как она решается конкретным инструментом.
Человек, владеющий несколькими инструментами, уже имеет возможность выбирать какой из них больше подходит для решения конкретной задачи.
Я страшно ненавижу C++
Я бы так не сказал. Я люблю С (т.к. начинал фактически с него, не считая немного фортрана и бейсика), потом появился С++ (в первых версиях "С с классами") - понравилось. Но сейчас мне не нравится то, куда идет современный С++ - туда пытаются втащить все на свете, что увидели в других языках (вместо того, чтобы работать над сокращением количества UB, которое только растет), разработчики компиляторов не успевают за писателями стандарта.
Как же этот язык, появившийся на сцене меньше десятка лет назад, стал настолько популярным?
А насколько он стал популярным? На мой взгляд популярность языка начинается тогда, когда появляется его устойчивый стандарт и язык активно используется в "большом энтерпрайзе". И, как ни парадоксально, когда появляется достаточное количество легаси кода, на нем написанного.
А пока в языке даже нет устоявшегося ABI... Использование его только на свой страх и риск.
Хотя, трудно спорить, в Rust есть действительно интересные концепции. Хотя с первого взгляда трудно сказать для какого класса задач эти концепции могут дать какое-то реальное преимущество перед тем что есть в других языках.
Откровения, снизошедшие на меня при изучении Rust в течение последних нескольких месяцев, трансформировали меня схожим образом.
Оценить плюсы и минусы инструмента можно только после того, как реализуешь им какие-то существенные проекты. А потом еще будешь поддерживать и развивать их в течении какого-то времени.
А это никак не "изучение в течении нескольких месяцев".
В целом я Вас понимаю. Сам через такое проходил неоднократно. Первое откровение снизошло еще в начале 90-х, когда столкнулся с Clarion (тогда еще под DOS) где можно было спроектировать таблицу, нарисовать экранную форму для работы с ней и оно само генерировало код.
Второй раз - чуть позже, но примерно в ту же эпоху. db_Vista - БД, основанная на сетевой модели (например, поддержка "наборов" где "запись типа А является владельцем набора записей типа Б") с ее DDL - Data Description Language - язык описания данных.
Последний раз - относительно недавно (17-й год) когда сменил работу и познакомился с платформой IBM i (AS/400). Которая абсолютно не похожа ни на какую другой ОС, построена на принципе "все есть объект" и поддерживает концепцию "интегрированной языковой среды" - ILE позволяющей для решения одной задачи одновременно использовать (в разных ее частях) несколько разных языков.
И каждый раз знакомство с новыми подходами расширяло кругозор и давало понимание того, что нет одного универсального инструмента на все случаи жизни, любой инструмент может быть удобен для одного класса задач и неудобен для другого. И для решения одной и той же задачи можно использовать разные подходы и каждый будет иметь свои плюсы и минусы.
С этой точки зрения уже рассматриваю каждую новую технологию - что даст ее глубокое изучение применительно к тем задачам, которые мне приходится решать? Стоит овчинка выделки? Короче говоря
LifeKILLED
25.10.2023 13:35Но сейчас мне не нравится то, куда идет современный С++ - туда пытаются втащить все на свете
Мне кажется, это должен решать каждый программист сам. Например, пользоваться только модерн C++, запретив себе использовать старые парадигмы после перехода на новый стандарт. Тогда код будет написан в одном стиле, понятен и легок в масштабировании. Получится фактически тот самый новый прекрасный язык с небольшим количеством нужных инструментов. И новый язык изобретать будет уже не нужно только для того, чтобы новички не "стреляли себе в ногу", просто потому что им в руки попалось ружьë. То есть искусственно ограничивать набор инструментов путем создания нового языка
Panzerschrek
25.10.2023 13:35+16Называть систему типажей в Rust утиной типизацией - это виртуозная ментальная гимнастика.
Как раз механизм типажей по своей сути - противоположен утиной типизации, ибо требует от автора типа реализовывать типаж утки, чтобы уметь крякать. Если фактически метод крякания есть, но типаж формально не реализован, не получится передать тип в код, где нужен этот конкретный типаж.
Истинная утиная типизация есть в шаблонах C++. Там можно в шаблонном коде вызвать у переданного типа метод крякания. И всё будет работать, пока в шаблон передаются типы с нужным методом и сигнатурой. А если нету нужного метода - компиляция сломается.
Каждый подход имеет как достоинства и недостатки. Но я лично склоняюсь к подходу C++, т. к. он всё же менее многословен.powerman
25.10.2023 13:35+1Когда типаж объявляется явно это гарантирует соблюдение не только наличия метода с нужной сигнатурой но и соблюдение семантики типажа - той части его поведения, которую невозможно выразить в сигнатуре и соблюдение которой не может проконтролировать язык.
Данное достоинство может показаться не критичным, по сравнению с крутыми фишками альтернативного подхода (работает само и иногда даже так, как автор и предположить не мог), но по сути это ровно та же проблема, что и в ситуации с ручным управлением памятью, которую хотели решить в Rust: человек делает ошибки, особенно там, где нужно быть очень внимательным и всё проконтролировать на 100%. Ручное соблюдение семантики типажа/интерфейса мельком (и не всегда полно) упомянутой в его доке всеми типами, авторам которых показалось полезным сделать поддержку этого типажа… по сути, это рулетка. В том же Go как раз утиная типизация, и я много раз ловил на ревью баги в реализациях даже "простейшего" (из одного метода) интерфейса io.Reader. И это при том, что уж что-что, а его доку в недостаточно чётком описании семантики обвинить точно нельзя. Но написать мало, ещё надо чтобы кто-то написанное прочитал, прочитал внимательно, целиком, и понял написанное. А с этим проблемы неизбежны просто в силу человеческой природы.
Я веду к тому, что для языка, который декларирует ценности Rust, утиная типизация здесь выглядит неприемлемой. И это принципиально, а не "ну, у всех подходов есть свои плюсы/минусы".
boldape
25.10.2023 13:35Все в общем то нормально с типажами за исключением того что я не могу реализовать не свой типаж для не своего типа. Да да очередная защита не понятно кого не понятно от чего, но факт в том что я даже не могу из за этого организовать СВОИ типы по разным модулям потому, что тогда меня начинает бить по рукам бланкет реализация. А ещё есть такие типы и такие трэйты которые НИКОГДА не встретят друг друга кроме как в вашем коде, какого хера мне запрещают реализовывать трэйты для типов в СВОЕМ модуле, добавьте ключевое слово или атрибут override что бы был явный опт ин в это и всё.
powerman
25.10.2023 13:35Я точно не знаю, но предполагаю, что это могут запрещать из следующих соображений:
Если автор типа не намеревался реализовать данный типаж, то даже если в данный момент так случайно получилось что типаж типом реализован (по сигнатуре методов), всё-равно нет гарантий, что будущие изменения реализации этого типа данную (не намеренную) поддержку не сломают (напр. изменением семантики этих методов). Соответственно, если кто-то посторонний у себя в проекте объявит что этот тип поддерживает данный типаж, то это может сломаться при обновлении библиотеки с данным типом.
Если дать возможность легко объявлять поддержку типом типажа в стороннем коде, то высоки шансы что это будут делать достаточно бездумно, механически, не проверяя насколько тип поддерживает семантику типажа, и в результате эта "фича" будет восприниматься разработчиками как "кривая поддержка утиной типизации, которая вроде и есть, но при этом требует почему-то писать лишний код ручками чтобы связать сторонний тип с типажом".
Возможно есть чисто технические сложности реализации этой фичи в компиляторе (если все поддерживаемые типажи известны в момент компиляции типа это может что-то где-то сильно упрощать). Отдельно этой причины может быть недостаточно чтобы отказаться от реализации фичи, но вместе с предыдущими это вполне может перевесить.
DarkEld3r
25.10.2023 13:35+1Да да очередная защита не понятно кого не понятно от чего
Eсли разрешить реализовать "чужие" трейты для "чужих" типов, то не ломающие (по semver) изменения очень даже смогут ломать компиляцию. Было бы совсем не весело получать сломанный билд при обновлении зависимостей, причём зависимости сами по себе ничего плохого как бы и не изменили.
Возможно, это можно было бы обойти, если бы мы не просто импортировали трейты, как сейчас, а требовались указывать для каких типов. Что-то вроде такого:
use lib_a::SomeTrait for crate::MyType, lib_b::AnotherType;
Но это значительно ухудшение эргономики. Плюс можно реализовать трейт для обёртки - обычно это не так уж страшно.
я даже не могу из за этого организовать СВОИ типы по разным модулям потому
По разным модулям - можно, нельзя по разным крейтам.
Panzerschrek
25.10.2023 13:35Я несколько не понял - а как явное указание реализации типажа защищает от багов в реализации?
На счёт защиты от ошибок - весьма слабая защита. Компилятор ведь тоже при реализации типажа проверяет только, что сигнатуры функций какие надо и что наличествуют нужные псевдонимы типов. Семантику, не выразимую через них, он проверить не может.
Польза от типажей в этом плане есть разве что в том, что явная реализация типажа заставляет быть чуть более внимательным и возможно даже читать документацию.powerman
25.10.2023 13:35Явное указание автором типа что тип поддерживает данный типаж предполагает, что автор типа не случайно по сигнатуре совпал с типажом (в этом случае высок шанс что сигнатура-то совпала а вот семантика типажа - нет), а намеренно реализовал поддержку типажа, т.е. и сигнатуру и семантику. От багов - да, это не защищает, но в такой ситуации если семантика нарушена то это именно баг, и его можно и нужно пофиксить, и фикс такого бага не будет считаться изменением ломающим обратную совместимость (т.е. может быть сделан в патч-версии а не следующей мажор-версии).
NotSure
25.10.2023 13:35+3"каждый проект, с которым я имел дело на C++, ощущался как монотонная рутина"
Значит, автор решал в своих проектах скучные задачи. Когда задача скучная, начинаешь играть с языком, чтобы хоть как-то веселее писать было. Для интересной задачи лучше, когда выражение идей в коде -- тривиальный процесс, а не квест.
cortl
25.10.2023 13:35-1В чём проблема написать if(smart_ptr1) ?
DarkEld3r
25.10.2023 13:35+2Звучит как "в чём проблема не совершать ошибок?"
cortl
25.10.2023 13:35-2Умные указатели для того и придумали, чтобы не совершать ошибок. Только их надо использовать с умом. Просто в C++ у вас есть выбор затрачивать ли ресурсы на проверку, что память жива или сделать быстрый алгоритм, если уверены. Не нравятся указатели используйте ссылки и значения.
Stalkerx777
25.10.2023 13:35Только их надо использовать с умом
А в чём их умность тогда? Они не достаточно умные чтобы не обнуляться или перестать быть валидными после std::move.
Всё что они делают в С++ это RAII но не гарантируют безопасной работы с указателями (но всё же лучше чем сырые указатели конечно)
Не нравятся указатели используйте ссылки и значения
Указатели в С++ тоже могут стать не валидными (dangling references)
cortl
25.10.2023 13:35-1Ни я, ни C++ не заставляем вас и кого угодно ещё использовать указатели сырые/умные, ссылки и мувы. Используйте переменные по значению! И будет вам счастье, если вы так хотите. Это ваш выбор. Весь посыл в том, что C++ предоставляет этот выбор.
KanuTaH
25.10.2023 13:35Они не достаточно умные чтобы не обнуляться или перестать быть валидными после std::move.
Вообще-то
unique_ptr
гарантированно обнуляются послеstd::move
, да и как может быть по-другому? Иначе они бы не были уникальными.inv2004
25.10.2023 13:35Всё же обнуление - это фича в данном случае. Могли бы не обнулять, а просто написать после move значение не определено
KanuTaH
25.10.2023 13:35+2Это уже из разряда фантазий. Список того, чего "могли бы" по сути бесконечен, нет смысла на него ориентироваться.
mayorovp
25.10.2023 13:35Не могли бы.
После перемещения объект должен оставаться в валидном состоянии (как минимум, деструктор всё ещё должен иметь возможность отработать!).
ZirakZigil
25.10.2023 13:35Они не достаточно умные чтобы не обнуляться или перестать быть валидными после std::move
Остающиеся в "unspecified but valid state" (c) moved-from объекты — by design. Если вам совсем-совсем не хочется их видеть после мува, то можно ограничить скоуп руками, это несложно и добавляет лишь немного шума.
KanuTaH
25.10.2023 13:35unspecified but valid state
Это в общем случае. Для конкретных типов из std состояние может быть вполне себе specified, как для того же
unique_ptr
.
domix32
25.10.2023 13:35если указатель переместили (std::move), то предыдущая локация станет по факту uninitialized и попытки читать что-либо из таких переменных является неопределённым поведением. В дебаге оно вам радостно занулит память, если повезёт, а в релизе вы с большой долей вероятности получите магическое поведение и рандомные сегфолты.
KanuTaH
25.10.2023 13:35если указатель переместили (std::move), то предыдущая локация станет по факту uninitialized и попытки читать что-либо из таких переменных является неопределённым поведением.
Нет, это не так. Во-первых, перемещение при конструировании - это как правило просто обмен состояниями, перемещающий объект получает состояние "свежесконструированного". Во-вторых, есть гарантии стандарта:
https://timsong-cpp.github.io/cppwp/n4861/unique.ptr#single.ctor-20
Это из C++20, но и раньше было так же.
domix32
25.10.2023 13:35-1Я скорее про ситуацию что выше рисовали.
auto ptr = std::make_unique<T>(); call(std::move(ptr)); if (ptr) { // <<< вот это по идее будет UB // some code }
Соответсвенно, от такого кода никакие проверки не спасут.
KanuTaH
25.10.2023 13:35+1вот это по идее будет UB
Нет. Я же выше даже ссылку на драфт C++20 кинул. Нет тут никакого UB. Откуда ему тут взяться?
APh
25.10.2023 13:35Интересно, что автор скажет про Питон, с высоты своего опыта освоения Раста?
В том плане, что ему нравится и не нравится в парадигме, концепции языков, несмотря на столь разные области применения?
И может ли кто сказать об опыте использования Раст для нейросетей и Дата Сайенс, вообще?
Dgolubetd
25.10.2023 13:35+9Не автор, но рефакторю сейчас большой проект на Python. Хочу сказать что надо на законодательном уровне запретить более N строк кода на нем писать, а в качестве наказания - гулаг.
Data Science - единственное для чего нет толковой альтернативы Питону, неизбежное зло.
ksbes
25.10.2023 13:35+1Это вы ещё левиафанов MATLAB не рефакторили ... с пользовательским интерфейсом и печатью в pdf ...
Вот уж реальная альтернатива Python (точнее наоборот - пайтон - это альтернатива матлабу, слава аллаху!)
WASD1
25.10.2023 13:35Я не начинаю писать на Python что-либо если моя оценка "больше 1000 строк кода".
1 KLoC оценки обычно превращаются в 2 KLoC написанного кода, включая нюансы.
Если сервис выходит за 5 KLoC - то после этого объёма прототип перестаёт быть нормально поддериваемым и каждое возвращение это боль и вспоминание "как вот это сделано".Теоретически - наверное на Python можно инженерить ПО. Но зачем тогда вообще Python если его киллер-фичу (возможность бысто запрототипировать) мы вообще не используем.
inv2004
25.10.2023 13:35+1Графомания. playground есть - вау, запишу в плюсы расту. Утиная типизация с "for Trait" - ага, ага, хотя бы определение в вики прочитать
bfDeveloper
25.10.2023 13:35+1Rust феноменально сильно влияет на мышление, но ничего действительно важного в статье не написано. Очередной раз сказали, что borrow checker это сложно, ну а дальше то что? Как поменялся подход к мышлению? Как по-другому проектирутся алгоритмы или архитектура? Может на других языках автор начал писать по-другому? Я только издалека посмотрел на rust, так и то интересные идеи почерпнул для своего C++ кода. Абсолютно пустая статья.
WASD1
25.10.2023 13:35+1То, что в раст свежая, прям "нулёвая" экосистема - это приятно. Вместо Make / CMake / Doxygen / ... просто cargo xxx. Но это может и протухнуть лет за 10.
Но давайте дадим слово начальнику транспортного цеха? Как всё-таки Rust меняет мышление?
Вот мне с Haskell понравилось (реально появилось много хороших приёмов в программировании).
А с Rust - мне кажется у меня ситуация обратная ожиданиям.Ожидание - написание программ на Rust это доведение до автоматизма практик написания кода, где некоторые классы ошибок в принципе отсутствуют.
Реальность - Rust настолько хоршо тебя контролирует, что ты даже забываешь думать о таких ошибках. И при переходе с Rust на привычные языки (скажем на C где у нас низкоуровневые библиотеки) - пару дней приходится заново привыкать следить за вещами, которые за тебя делал Rust.
grigorym
25.10.2023 13:35Первое впечатление от статьи: знающий haskell и умеющий в unique_ptr<> в плюсах обнаружит, что ему всё уже знакомо. Правильное или обманчивое впечатление?
WASD1
25.10.2023 13:35Вы про Rust или про статью?
Если про Rust - то, да будет кардинально проще (если придираться контрольного "изучить Rust не зная Haskell и C++").Хотя от понимания машинерии многих мелких нюансов это вас это не убережёт.
sergey_privacy
25.10.2023 13:35-2Автор, если тебе после C++ понравился Rust, то попробуй еще PHP. Через год Rust и другие языки типа Go, Python, C#, Java будут казаться архаичными, неудобными, странными. Привыкнув к PHP ты не захочешь пересаживаться на что то еще, как человек, катавшийся на Лексусе или Порше вдруг сядет в ржавую "копейку". У меня было "за плечами" штук 15 языков программирования, начиная от скриптов, basic-а и ASM, заканчивая 1C и SQL. До этого скакал с языка на язык быстро и легко. После перехода на PHP, пытался поглядывать в сторону руби, питона, го, эрланга - не получается себя заставить обратно загнать в убогие неудобные рамки.
WASD1
25.10.2023 13:35+4э... это какая-то очень тонкая, понятная лишь избранным ирония?
Если так - поясните для более приземлённых пользователей Хабра.
JordanCpp
25.10.2023 13:35-2Но потом я понял. Rust позволяет мне делать то, что я хочу, но сначала просит подумать, действительно ли я хочу именно этого, и заставляет задуматься о последствиях моего решения.
Аллилуя!
Теперь когда я пишу код на других языках, то думаю о том, какие объекты хранят ссылки на какое значение, где на значения ссылаются или где их копируют, а также о влиянии этого на производительность и надёжность.
Впусти господа в сердце.
Rust делает меня более совершенным программистом, даже когда я пишу код на других языках.
Аминь.
Тег сарказм:)
Sild
Да-да, сверху выглядит очень круто. А потом начинается обмазывание явным указанием времени жизни, десятком трейтов которые нужно протащить (Debug, Send, Sync, WhoKnowWhatElse), .unwrap()-ы на пустом месте...
Мне тоже понравилась его экосистема и управление памятью. Даже сам процесс сборки можно кастомизировать раст-кодом (build.rs ) - это ж огонь!
Но любить раст за то что можно сделать полиморфизм на трейтах вместо интерфейсов - странно.
nikis05
Явные указания времен жизни на самом деле нужны довольно редко, в 80% случаев если они понадобились то человек занимается несвоевременной оптимизацией или пытается выстрелить себе в ногу. Покажите кстати пожалуйста какой-нибудь кейс если не лень где вам понадобились явные указания времен жизни.
Не
unwrap
айте на пустом месте :) Обработайте ошибку нормально, а если не хотите - пусть компилятор заставит вас написать шесть букв для явного, осознанного опт-аута, и правильно сделает. И заодно когда ваша очевидная и точно-точно правильная эвристика на тему того, почему тут никак не может бытьNone
, таки окажется ошибочной, в сообщении об ошибке будет указание файла и строки, где она находится.Косяки в языке есть, но не совсем те которые вы перечислили.
muturgan
Без сарказма - а какие бы косяки перечислили бы вы? Хочется лучшего представления о языке.
mayorovp
Тяжело работать с древовидными структурами данных. Вроде и всё правильно компилятор пишет, а работать всё равно тяжело.
С интрузивными двусвязными списками какая-то ерунда, самый нужный случай не покрыт библиотекой intrusive_collections.
Математические концепции нормально не выразить, такие трейты как "вещественное число" или там "кольцо" нужно писать самому (а потом подгонять под примитивные типы макросами!)
nikis05
Первое: отсутствие negative trait bounds и специализации.
Поскольку в языке очень много завязано на трейтах и blanket implementations (которые автор оригинальной статьи называет "утиной типизацией"), то это прям досадный косяк, приходится идти на всякие хаки чтобы его обходить.
Пример на который я натолкнулся буквально вчера: есть такая популярная библиотека anyhow. Она экспортирует тип
anyhow::Error
, в который можно конвертировать любой конкретный тип Error из любой библиотеки, или просто текстовое сообщение, и т.п. В результате у вас получается один тип для всех ошибок, иногда это удобно / нужно, ну и там есть всякие удобные мелочи типа возможностей добавлять контексты. И есть трейт из стандартной библиотекиstd::error::Error
, который рекомендуется реализовывать всем типам ошибок. Теперь внимание:В результате
anyhow::Error
вынужден прибегать к хаку, чтобы его можно было использовать там, где ожидаетсяstd::error::Error
. Они это делают через AsRef<dyn StdError> + Deref<Target = dyn StdError>, последнее считается антипаттерном в языке.Но работа в этом направлении ведется, обещают добавить.
Второе: публичные поля и геттеры.
Мелочь но бесит. Если вы делаете что-то типа
то вы даете гарантии насчет implementation details вашего типа, потому что если вы потом захотите сделать что-то вроде
то у зависимого кода везде сломается
foo.bar
. Поэтому почти все библиотеки включаяstd
не используют публичные поля практически вообще, а используют геттеры, которые компилятор просто потом заинлайнит. Я бы запретил публичные поля в принципе, и добавил бы лучше какой-нибудь специальный упрощенный синтаксис для геттеров.Третье: отсутствие приватных методов в трейтах
Иногда нужно сделать что-то такое:
Пример немного синтетический но лучше не придумал :)
Мысль в том что у некоторых трейтов есть один-два "метода для реализации", которые предназначены только для того чтобы тип мог реализовать трейт. И большое количество "методов для вызова", которые потом вызывает код который с этим третом работает. Спрятать "методы для реализации" от вызывающего кода на сегодняшний момент можно только через уродливый хак (супертрейт), который сливает implementation details и который к тому же вроде как собираются запретить в будущем.
Четвертое: макросы
Мне в целом нравится как макросы сделаны в Rust, но их просто невозможно нормально отлаживать, на сегодняшний момент лучший вариант - это
cargo extend
, который выплевывает всю вашу кодовую базу с "развернутыми" макросами в stdout, и вам потом надо ковыряться в поисках того места где макрос выплюнул неработающий код, при чем без IDE. И копипастить куски этого кода в ваш остальной код, чтобы посмотреть, что не компилится, и потом убрать. Бесит страшно, и было бы круто если бы был какой-то инструмент, позволяющий посмотреть, во что макрос разворачивается.Второй косяк макросов - они во многом регулируются не языком, а негласным сводом правил приличия. Иногда авторы библиотек делают неправильно, и например их макрос ломает остальные макросы. Или они изобретают какой-то причудливый синтаксис, который ломает автоформатирование во всем файле. Также каждая библиотека немного переизобретает эти конвенции, поэтому похожие макросы могут работать по-разному, поэтому рутинна например ситуация, когда в трех разных библиотеках будет работать один из трех вариантов, но не остальные:
Пятое: Send + Sync
Не проблема имхо с "проброской" трейт баундов в целом, но это сочетание встречается уж очень часто. Если у вас асинхронный код, вы будете использовать рантайм, рантайм будет хотеть перекладывать
Future
с одного треда на другой, для этого ему нужно будет чтобыFuture
былSend + Sync
, для этого любые данные внутриFuture
должны бытьSend + Sync
. Если у меня такое приложение где у меня multithreaded async рантайм, то ну нигде у меня не будетFuture
которые неSend
и неSync
. Я буду использоватьArc
вместоRc
иMutex
вместоRefCell
. Ну позвольте мне один раз включить этот баунд везде, почему я должен каждый раз писать его?Шестое: время компиляции
На большом проекте (веб бэкенд с где-то 300 эндпоинтами) иногда сидишь как осел по 20-30 секунд после каждого
Ctrl+S
. Уже начинаешь ненавидеть и этот язык, и эту работу, и эту жизнь. Это было на аймаке 2017 года (4 ядра, 3.8ггц, 8гб), пришлось проапгрейдиться, пока полет нормальный.Еще есть всякая неприятная мелкота типа невозможности использовать
foo: impl Trait
в некоторых контекстах, отсутствие поддержки async traits в языке и т.п, но все эти вещи вроде как в работе.Я со своей колокольни как бэкендер и полный лапоть в более низкоуровневых вещах, у тех кто из C / C++ будет еще вагон своих претензий, на Reddit можно почитать по запросу "reddit what you don't like about rust".
DarkEld3r
Я как раз из С++ в раст пришёл, поэтому данная проблема меня совсем не смущает. Если метод переименовать или сделать приватным, то случится точно такая же проблема. Опять же, очень удобно использовать структуры с публичными полями просто как набор данных без ассоциированных методов. Для таких случаев уже геттеры/сеттеры будут выглядеть лишняя сущность.
С точки зрения "красоты" кода, возможно, имело бы смысл запретить смешивать публичные и приватные поля - доводилось встречать и такое.
SpiderEkb
Помнится в Borland C++ Builder была интересная сущность - __property
Если CurrentTrack используется как lvalue, вызывается SetCurrentTrack с rvalue в качестве аргумента, а если как rvalue - вызывается GetCurrentTrack...
DarkEld3r
В D нечто подобное есть(
@property
), но более многословное. В С# немного "наоборот": там вызывать всё-таки get/set приходится, зато сахар для объявления, кажется, наиболее короткий.SpiderEkb
Ну вот дальше билдера в С++ это не пошло. А жаль.
Причем, там если написать
или
то получаем write only или read only проперть.
Естественно, что там в геттере или сеттере совершенно неважно. Т.е. геттер мог ее из БД читать, например, в сеттер в БД писать...
Помнится, для виндовых ini файлов такое удобно было в частности - геттер читает из файла, сеттер пишет в файл
Heggi
В шарпах как раз так же:
IvaYan
Нет, вызывать явно ничего не нужно
Код выше сам сгенерирует то, что на тамошней терминологии называется backed field. И сгенерирует геттер и сеттер. Либо можно в явном виде:
Использовать можно так:
DarkEld3r
Спасибо, я почему-то думал, что сгенерятся как раз
getMyInt
/setMyInt
.withkittens
К слову, методы
get_MyInt
/set_MyInt
действительно генерятся:Но пользоваться напрямую ими нельзя, да и в autocomplete они скрыты:
lennylizowzskiy
В Котлине всё же наиболее короткий, ибо все field по умолчанию являются property
Управлять геттерами и сеттарми вручную можно аналогично с тем как это делают в Шарпах:
DarkEld3r
Симпатично, правда не уверен, что правильно понял вот этот пример:
Тут будет сгенерирован сеттер, который будет устанавливать значение
a
, но прочитать значение будет нельзя так как всегда будет возвращаться 2?nikis05
Да, но это уже целенаправленное изменение API.
А тут получается: во-первых, дается гарантия насчет имплементации. Тип - структура а не enum или что-то еще, и у него есть такое-то поле. При чем поле это на самом типе а не вложено где-то внутри, и поле это не вычисляемое и не за каким-нибудь смарт пойнтером.
Во-вторых, гарантия эта для вызывающего кода по большому счету бесполезная - ему то какая разница, геттер это или поле, он просто хочет данные прочитать.
В третьих, гарантия эта обычно не целенаправленная, а случайная: не то что это специально так задумано, а просто поленился / пока не посчитал нужным написать геттер.
Для «примитивных» структур, которые работают просто как именованные кортежи, не имеют внутренних инвариантов и так далее, публичные поля - норм, но для таких структур тогда все поля должны быть публичными. Писать
pub
перед каждым полем - подбешивает.И опять же если понадобится такую структуру поменять из примитивной на менее примитивную, это сломает зависимый код, хотя по сути это implementation detail. Так что в идеале я бы добавил ключевое слово для простого создания геттеров, и стер бы разницу между геттерами и полями как таковую.
DarkEld3r
По большому счёту согласен, но в целом с "целенаправленным изменением апи" не всё так просто. Не зря есть штуки вроде
cargo-semver-checks
- порой случайно сломать публичное апи легче, чем кажется.mayorovp
Не бесполезная там гарантия.
У поля можно не только прочитать значение - но и взять на него ссылку. Причём если у вас есть два поля - вы можете взять мутабельную ссылку на каждое, что для акцессоров невозможно.
nikis05
Вы правы, совсем забыл про это - но зачем это может быть нужно вне модуля самого типа?
DarkEld3r
Теперь мне любопытно в чём разница и почему в модуле это может понадобиться, а вне - нет?..
А понадобиться может если мы не хотим везде передавать целую структуру, если нужно модифицировать только отдельные поля:
Да, пример максимально синтетический и мне проблема тоже кажется преувеличенной.
muturgan
Спасибо за развернутый ответ)
domix32
Лайфтаймы обычно появляются, когда появляется более обобщённая библиотека. Ну и вторая проблема, что если лайфтам или trait bound появился в одном месте в коде, то он с большей вероятностью будет отравлять всех пользователей необходимостью писать этот же самый баунд/лайфтайм у каждого импла. Рефакторинг из-за этого превращается в некоторый локальный адок.
domix32
Так трейты суть интерфейсы.
DancingOnWater
Нет, они что-то сродни концептам.
DarkEld3r
В дженериках - да, но
dyn Trait
- это вполне себе интерфейс.DancingOnWater
Ключевое слово dyn достаточно хитрое. По факту оно создает новый тип "dyn Trait", который содержит указатель на данные и указатель на методы. Но, помимоо этого вы можете сделать:
У типажей нету механизма наследования когда вы пишите:
Вы не наследуете TraitB от TraitA, вы накладывает ограничения, чтобы тип определяющий TraitB также определял бы и TraitA. И это две большие разницы.
mayorovp
Да нет никакой разницы, "наследование" интерфейсов работает по сути так же.
DancingOnWater
Нет, при реализации трейта вы не можете использовать методы из реализации соседнего трейта, только методы и данные самого типа. Т.е. если учть продолжить пример у вас будет:
unC0Rr
Не вижу проблемы: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f7d70c704d93ab951198c1dbeb93b65e
DancingOnWater
Значит уже затащили в стабильную ветку, где-то еще год назад такой фокус не проходил. Запаривало страшно.
DarkEld3r
И? Чем плоха возможность делать
impl dyn Trait
?Можно эту разницу увидеть на примере?
Основное ограничение трейтов, которое приходит мне в голову: нельзя из
dyn Child
перейти кdyn Parent
. Хотя может когда-то и сделают.DancingOnWater
Да ничем, более того это единственный вариант, когда можно реализовать кастование dyn объектов пока не завезли в стабильную ветку. Но то, что в ООП называется интерфейсами в Расте возможно только через dyn объекты, а их не для каждого типажа можно создать.
Вот в такой конструкции вы не сможете создать dyn TraitA, а из-за это не может создать dyn TraitB, также мы не можем создать dyn TraitC для типа А, но можем для типа B.
mayorovp
А где в этом примере разница между наследованием и наложением ограничений?
А что мешает?
DancingOnWater
Мешает то, что тип А реализует TraitA. Этой ситуации не может быть в принципе при наследовании.
mayorovp
Чем именно оно мешает-то? Вот такой код у меня спокойно компилируется, если реализацию трейтов добавить:
DancingOnWater
Спасибо, мне казалось что добавит ограничения и на сам тип. Был не прав. Но в любом случае вы не сможете сделать let a: &dyn TraitA = &A{};
mayorovp
Разумеется, потому что TraitA - не интерфейс (с точки зрения ООП). Как и TraitB (если бы он был интерфейсом, он бы наследовал TraitA, а TraitA не интерфейс).
Всё ещё никаких отличий наследования интерфейсов от накладывания ограничения...
DarkEld3r
Всё-таки пример несколько синтетический. Ну и вот так вот можно:
domix32
Это только в случае если мы их в качестве границ типажа указвыаем - как раз эти вот всякие
Send+Sync
. Если же оно как `impl/dyn Trait` в качестве параметра, то это вполне себе интерфейс.Ilirium
Интерфейсы, трейты, концепты, прототипы, и классы это разные подходы к реализации идей объектно-ориентированного программирования, между ними много общего, но и достаточно много различий.
Интерфейсы, если брать их реализацию в Go супер ограничены в сравнении с трейтами в Rust. На трейтах можно имитировать интерфейсы, но их ограничения придется соблюдать руками, компилятор не будет ругаться.
Стараясь разбираться, в чем принципиальная разница между концептами в Swift и трейтами в Rust, я к сожалению не смог понять, нужно начать программировать хотя бы немного на Swift, но на это у меня не было времени.
Классы целиком реализуют идеи ООП: наследование, полиморфизм и инкапсуляцию. Но из-за реализации полиморфизма через наследование имеют ограничения, особенно если язык не позволяет множественно наследование, что на практике приводит к проблемам в больших системах. Из того, что я понял, трейты и концепты избавлены от этой проблемы. ООП через классы было популяризованно языком Smalltalk, часть его разработчиков потом создали Self, который я так понимаю ввели идею прототипов, легшую в основу трейтов и концептов.
domix32
На самом деле надо смотреть не на название, а на идею, которые они реализуют. Интерфейсы - пытаются реализовывать некоторый API к некоторому поведению. Типажи в Rust, интерфейсы в C# и Go, абстрактные классы в Java/C++, протоколы в Swift - это как раз все про поведение. Например Iterator/Iterable - яркий пример такого поведения.
Вторая концепция - ограничение на поведение. У раста это соотвественно
lifetime bound, довольно уникальный механизм среди языков.
static qualifier
и DreamBerd не в счёт.разветвлённая система trait bounds : `where T: Send+Sync+Blabla`
type bounds `call<T: Send>(var: T) -> _`.
У С++ есть <type_traits> и собственно в будущем должны будут выступать концепты плюс также type bounds (`template<typename T=Foo>`). В качестве управления лайфтаймом можно назвать какой-нибудь кастомный аллокатор, но строгости и эргономики такой как в Rust там не добиться. Пропозалы по лайфтаймам вроде были, но я насколько он жив и развивается не знаю. К с++37 глядишь появится.
О существовании аналогичных ограничителей в C# и Swift мне доподлинно не известно, но скорее всего в свежих редакциях языков наверняка есть или планируется что-то аналогичное ржавым трейтам и плюсовым type bound.
В го генерики только появились и едва ли их будут усложнять подобными штуками дабы не усложнять язык либо будут существовать только как внутренности компилятора.
Третья концепция - это то как поведения компонуются с объектом и перегружаются им тут уже начинаются различные способы - наследование(class A derive B), композиция (impl A for B), расширение (class A extends B). Всё это дружно приводит к появлению виртуальных таблиц в каком-то виде и возможностью переопределять его в пределах конкретного объекта, давать доступ к полям объекта, вызывать всю иерархию поведения (а ля `super(B, self).parent_method()` ). Про это была помнится обширная статья на хабре про модели полиморфизма в различных языках.
В итоге получаем кучку концепций, которые в том или ином виде приклеиваются к одной сущности в разных пропорциях и получаем конфуз с тем что типажи мол это де не интерфейс, а граница типа, а не как вот в этих Go/С# тоненькая штука.