Как стать автором
Обновить

Комментарии 73

в качестве черновика перевода, сгодится. если кратко: необходима вычитка.

Язык который не позволяет сделать простые вещи просто - тупиковая ветвь. Но это видимо не очевидно. Раст яркий пример когда взяв всего один приём пытаются им решить все вопросы, но при этом создают проблемы с которыми придётся иметь дело позже. И если "в системе типов есть всё" есть много желающих запихать туда еще больше. В результате распухание сложности гарантировано. При этом вообще-то никак не решена проблема разрастания зависимостей. Зато надо следить за большим количеством сущностей.

https://youtu.be/bKyxOaP-mDg?t=3120

Абстрактный наброс. Будут примеры?

этом создают проблемы с которыми придётся иметь дело позже

много желающих запихать туда еще больше.

При этом вообще-то никак не решена проблема разрастания зависимостей

Можно и не добавлять зависимости. Либо напишите, как надо решить, мы RFC-ку расту напишем.

Ох, эти американизированные статьи и книги по деланью чего-то очень забавные. Можно как в фильмах Марвел поменять героев, оставив сценарий, вот и новый фильм на миллиард баксов сборов готов. По такому же принципу работает и чтиво. Можно поменять тему, но основу книги/статьи оставить - вот и новый контент.

P.S.
За перевод спасибо :)

Неужели rust стоит того, программируя на нем, проходя через все эти препядствия? Переосмысливать все программирование и претерпевать эти страдания с новыми конценпциями? Мне кажется, что не cтоит.

Тысячи уязвимостей в сишных программах наглядно демонстрируют что стоит

Не только замена C/C++ но и Java/C#/Go

Rust имеет очень неплохие фичи из функциональщины, что полезно в корпоративных бизнес системах, системах со сложными бизнес процессами.

Порог входа для них высокий, ага

IDE + LLM в помощь!

Порог входа мне как раз высоким не показался. Мне показалось, что я не понимаю, какие именно нерешенные задачи решает этот язык. Даже у Го есть идея: горутины, хоть какой-то параллелизм из коробки, который к версии 2.0 даже может стать более-менее работоспособным. А зачем мне в мире, живущем по законам Мура раст?

какие именно нерешенные задачи решает этот язык.

Сочетание безопасности и скорости (языки с GC безопасные но медленные, околосишечные языки быстрые но небезопасные)

в мире, живущем по законам Мура

Во-первых, закон Мура вроде как несколько раз хоронили

Во-вторых, судя по этой фразе, именно из-за таких людей как вы появился закон Вирта)

языки с GC безопасные но медленные

Иными словами, докер написан на медленном языке, но на расте почему-то пока не переписан. Хотя там можно бабла нарубить как нвидиа на гпу. Почему так, не подскажете?

околосишечные языки быстрые но небезопасные

Вы пробовали написать низкоуровневый код на расте без unsafe?

А зачем докеру "быстрый" язык? Он же только управляет контейнерами, нагрузки на него нет. Его можно было хоть на Питоне писать.

И как вы собрались рубить бабло с бесплатной программы?

Ну какой вопрос такой ответ

Кроме управления памятью, полезные фичи Rust по сравнению с популярными языками (Java, C#, C++):

1. Безопасность многопоточности на этапе компиляции (отсутствие гонок данных).

2. Мощная система типов и алгебраические типы данных (Option, Result).

3. Встроенный pattern matching.

4. Макросы и метапрограммирование.

5. Отсутствие null (использование Option).

6. Выражения вместо инструкций (более лаконичный код).

7. Простая интеграция с C-библиотеками без накладных расходов.

8. Cargo — удобный встроенный менеджер пакетов и сборщик проектов.

9. Строгая модель владения и заимствования ресурсов (полезна не только для памяти, но и для любых ресурсов: файлы, сетевые соединения и т.д.).

🧮 Модель: #GPT-4.5 )))

То есть киллер фича это управление памятью и zero cost abstractions, но во многих областях он выигрывает по очкам

1. Безопасность многопоточности на этапе компиляции (отсутствие гонок данных).

Сами пробовали? Рекламные брошюры я перестал читать еще в школе. Вот простая и довольно частая задача: засосать CSV из файла с валидацией и группировками.

2. Мощная система типов и алгебраические типы данных (Option, Result).

Ух ты. А почему это хорошо?

3. Встроенный pattern matching.

О, да. Весьма среднего качества, правда. Но в 2025 это достижение, конечно, на фоне остальных гошечек.

4. Макросы и метапрограммирование.

Нет. Пока нет, по крайней мере. Метапрограммирование в расте в зачаточном состоянии.

5. Отсутствие null (использование Option).

О как. А почему это хорошо?

6. Выражения вместо инструкций (более лаконичный код).

Обалдеть! Выражения вместо инструкций! Вот бы Джону Маккарти и Стиву Расселу кто-нибудь это посоветовал в 1960 году!

7. Простая интеграция с C-библиотеками без накладных расходов.

А у самого си она сложная? А у кристала, го, зига, д? А накладные расходы прям так гиперужасны? А их кто-нибудь измерял в цифрах?

8. Cargo — удобный встроенный менеджер пакетов и сборщик проектов.

Чуваки сделали пакетный менеджер? Не, ну это заявка, конечно.

9. Строгая модель владения и заимствования ресурсов (полезна не только для памяти, но и для любых ресурсов: файлы, сетевые соединения и т.д.).

Идея заимствования ресурсов — изящна, тут спору нет. Но не настолько, чтобы ради нее прям язык выбирать.

  1. Пример неудачный) Эффекта параллельного парсинга не будет, остальное c# сделает лучше.

  2. Почему это хорошо:

Компилятор заставляет явно обработать все возможные случаи.

Ошибки и некорректные состояния становятся явными и проверяются на этапе компиляции.

Код становится безопаснее, чище и понятнее.

Пример не буду приводить. C#, Java есть, но хуже. В Go нет ADT.

3. Согласен

4.Ok, я не в теме

5. Лучше. Чем грузины. Придётся писать слишком длинный ответ.

6.В Rust почти всё — выражение, т.е. даёт значение, которое можно сразу присвоить или вернуть

let tax = if income > 50_000 { 0.20 } else { 0.15 };          // if-выражение
let size = match list.len() { 0 => "empty", 1..=3 => "few", _ => "many" }; // match-выражение
let n = { let a = 2; a * a };                                 // блок-выражение

Railway-pattern = цепочка «чистых» функций Result/Option → Result/Option.

В Rust каждое звено — обычная expression-функция

7. Интеграция с go проще чем на c#, java и примерно одинакова с Go. Python - C тоже удобно.

8.Cargo меньше конфликтов версий, воспроизводимость «из коробки» и встроенные механизмы защиты цепочки поставок. Отлично интегрирован в IDE rustrover. Лучше чем в с#

9.Идея заимствования ресурсов и отказ от GC позволяет например держать высокий и стабильный RPM без пауз с высокой P95 и P99

И добавлю

Хотите полноценные ADT + pattern matching и иные развитые ФП абстракции «из коробки» — смотрите на семейство ML и его наследников. Rust обгонит по популярности их всех. Хотя c# почти догнал по фичам и более популярен.

А зачем мне в мире, живущем по законам Мура раст?

Исключительно «безопасное» ручное управление памятью. Предупреждая следующий вопрос древнекитайской философии «анахуаэто?» — я вижу только два применения:

  • Классические задачи ручного управления памятью — встройки, ответственное low latency и т.д.

  • Универсальные высококачественные библиотеки, которые используются в нескольких экосистемах (например, BLAS, libZ, какое-нибудь middleware, etc).

Ну вот, наконец, и разумный ответ на провокационный вопрос :)

Это всё так, но мир вокруг нас перестраивается с однополяр^W одноядерного на многоядерный. И раст мог бы бросить все усилия на примитивы параллелизации. А они вместо этого память в энумах выравнивают, выгрызая наносекунды на промахах кэша.

В обеих упомянутых областях не так и важно, на чем написать высокоответственный код. Его сравнительно немного, его понятно, как отлаживать. Ну займет возня с внезапной утечкой в си на пару дней больше — и чё?

Согласен.

Раст после того как его по факту не взяли в Линукс мог бы сосредоточиться на параллельности и более продвинутом ФП (паттерн матчинг, завтипы, ...) и пойти в бизнес приложения

Могу сказать, что это намного проще, чем учить С++ или пользоваться окаменевшим в мезозое С.

Я после 16 лет с JS/TS/Node перешел и мне нравится. Пишешь код, а половину тестов пишет за тебя компилятор. И предсказуемость выше, для финансового софта вообще прекрасно. И иммутабельные енумы со стейтом-объектом вообще прекрасны для написания кода. Я как-нибудь напишу статью про стейт-машины и Rust.

Напиши

Если нужно простое решение - решайте простыми инструментами, они некуда не деваются, их никто не заменяет. Например, ядро linux - это не простое решение, поэтому это не решается простыми инструментами. Иными словами просто появляется инструмент который занимает свою нишу. Он не идеален, но развивается. Возможно появится другой инструмент который сможет гораздо круче решать разные классы задач, и он будет учитывать этот опыт, поэтому пройти эту эволюцию человечество должно.

Никак не пойму, зачем сделано так неудобно.

1 Пример 1й. fn longest(.

1.1 Почему компилятор, отлично умеющий высчитывать лайфтаймы, не может здесь сделать автовывод лайфайма?

1.2 функция чистая, зачем вообще в ней вычисляется владение?

Потому что это не вычисление lifetime, а гарантия lifetime. То есть передавая пойнтер на переменную вы должны гарантировать, что она будет существовать в процессе выполнения.

Чего, как она может перестать существовать в процессе выполнения подфункции?

В данном конкретном случае без понятия, но dangling pointer все ещё является большим источником проблем. Механизм lifetime это в том числе попытка его полностью исключить.

Из другого треда удалится?

Не может, владение не даст.

Погодите. Но как раз таки даст. Если вы описываете владение таким образом, то чем это отличается от GC?

И каким образом я описываю владение? Я пользуюсь терминологией раста

Проблема не в самой функции, а в лайфтайме результата.

Компилятор требует от программиста написать, что результат функции существует, пока существуют оба её аргумента. Странно только, что автовывод лайфтайма не сделали в такой ситуации. Похоже, что это ломает логику в других местах.

Это ломает не логику, а принцип достаточности сигнатуры функции. В противном случае анализ лаймтаймов перестаёт быть локальным и становится глобальным.

Потому что правила автовывода лайтаймов (lifetime elision) строго определены, и данный случай под эти правила не подходит.

Вот пример кода, где выставление одинакового лайфтайма (времени жизни, ВЖ) во всех местах в функции foo_incorrect не пройдет проверку БЧ (borrow checker):

// https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1ce9cb09c241bf12fba2bd33574b24b0

fn main() {
    let mut str_mut = "";
    
    let s1 = String::from("str1");
    {
        let s2 = String::from("str2");
        str_mut = foo_incorrect(&s1, &s2); // fail here
        // str_mut = foo_correct(&s1, &s2); // will be ok
    }
    
    println!("{str_mut}");
}

fn foo_correct<'long, 'short>(a: &'long str, b: &'short str) ->  &'long str {
    todo!()
}

fn foo_incorrect<'a>(a: &'a str, b: &'a str) ->  &'a str {
    todo!()
}

Надо понимать, что БЧ выполняет проверку ВЖ для каждой функции отдельно, не заглядывая в тела других использующихся функций. То есть, в примере выше - неважно что будет написано в теле функций foo_correct \ foo_incorrect - проверка ВЖ для функции main от этого никак не изменится: учитываться будут только сигнатуры вызываемых функций.

Если бы для каждой функции анализировались тела всех используемых функций, то время компиляции стремилось бы к бесконечности. Но что хуже - такое поведение вносило бы гораздо больше ошибок из-за неправильного вывода ВЖ. Даже стандартные правила вывода ВЖ иногда приводят к неверному результату, приходится вручную указывать (но это редко).

Компилятор не может знать что вы хотите написать, он может только проверить на корректность написанное.

Пример 2 my_func(s); my_func(s);

2. Почему переменная сгорает (потребляется). Ну передали владение параметру (опять же константное), но функция отработала и Вернула долг!

В итоге я не могу написать просто так my_func(s); Trace(s);

И зачем?

В примере компилятор понимает заданные пользователем указания, что возвращаемая ссылка живет не дольше, чем кратчайший из лайфтаймов входящих ссылок. Явное лучше неявного, а компилятор подскажет.

А если не уверены то просто копируйте

fn longest_owned(x: &str, y: &str) -> String {
    if x.len() > y.len() { x.to_string() } else { y.to_string() }
}

Вот именно, должен и сам это понимать. Без лишних вопросов

Вы хотите вывести сигнатуру функции из тела функции — это гиблый путь, при котором любое мелкое изменение в функции может сломать обратную совместимость

Окей, принимается. Особенно в случае раздельной компиляции, лайфтайм является частью сигнатуры. Которую в текущей реализации приходится менять руками явно.

В случае же автовывода, скажем, для лямбд, это уже, видимо не такой уж гиблый путь ввиду локальности изменений.

Лямбды тоже можно таскать глобально по всей программе
struct LambdaHolder {
    func: Box<dyn Fn(i32) -> i32>,
}

fn not_lambda(a: i32) -> i32 {
    a + 2
}

fn give_me_your_lambda(l: &mut LambdaHolder, b: i32) {
    l.func = Box::new(move |a| { a + b });
}

fn main() {
    let mut l = LambdaHolder {
        func: Box::new(not_lambda),
    };
    println!("{}", &(l.func)(2));  // 4
    give_me_your_lambda(&mut l, 5);
    println!("{}", &(l.func)(2));  // 7
}

Нормально, одна единица трансляции, всё на виду у компилятора.

Проблемы с сигнатурами (если был автовывод и не учтен в манглинге ) могут возникнуть если в разных модулях будет разный неучтенный лайфтайм.

Ну теоретически всё ещё можно пробросить pub struct LambdaHolder или pub fn give_me_your_lambda в стороннюю библиотеку (но писать ещё один громоздкий пример я не буду)

А делать две разных логики для публичных и приватных функций — по-моему только ещё больше всех запутает

Да, вероятно с помощью dyn, можно будет сломать безопасность. Захват по ссылке лямбдой как источник проблем, где то это уже было =)

но функция отработала и Вернула долг!

В сигнатуре функции нет такой гарантии, будущие версии этой функции могут изменить своё поведение. Если программист хочет гарантировать, что строка будет жить после работы функции - он должен явно объявить такую гарантию в сигнатуре этой функции

И зачем?

Чтобы пользователь этой функции мог полагаться на явно объявленные гарантии, а не что-то там гадать про наличие или отсутствие долгов

Гарантия уже есть, описана сигнатурой. Иммутабельная переменная, иммутабельное содержимое.

Параметр передаётся не по ссылке.

Параметр передаётся не по ссылке.

Именно! А это означает, что функция получает полное владение параметром и, если захочет, может уничтожить его. А значит из этой гарантии следует, что использовать параметр после вызова функции нельзя (иначе рискуем получить use-after-free).

Есть ломающий пример? Как функция с иммутабельным параметром может его уничтожить

Так он есть прямо в посте:

fn my_func(v: String) {
    // делаем что-то с v
}

После вызова my_func(s) владельцем параметра становится функция. Когда работа функции завершается, у параметра больше не остаётся владельца. Раз владельца больше нет — объект автоматически уничтожается и освобождается связанная с ним память. Этот факт защищает от утечек памяти, а невозможность использовать параметр повторно — от use-after-free

Я понимаю, что владение передаётся. И потом уничтожается.

Я не понимаю, зачем оно передаётся (в случае иммутабельного параметра для начала) ? Это неудобно

Это вопрос не к реализации владения, а к дизайну языка

зачем оно передаётся

Потому что программист так захотел и именно так объявил в сигнатуре функции

Как функция с иммутабельным параметром

(в случае иммутабельного параметра для начала)

Я тут у вас вижу повторяющееся заблуждение. Владелец параметра может делать с ним что угодно, в том числе превратить иммутабельное значение в мутабельное:

fn my_func(v: String) {
    let mut v = v;
    v.push_str("мутирую азаза");
}

Наличие или отсутствие мутабельности у значений — это больше защита от случайных опечаток, чтобы программист случайно не сделал то что изначально не планировал (но если всё же запланировал, то он может явно объявить мутабельность как в примере выше)

А вот мутабельность в ссылках это будет уже совсем другой разговор

Т.е отбрасывание иммутабельности даже не требует unsafe o_O

Не поверил, полез на плейграунд.

Тогда логика есть. Перректальная правда

Ну, это не создаёт никаких уязвимостей, связанных с повреждением памяти, так что нет технических причин объявлять это unsafe ¯\_(ツ)_/¯

Вообще то создаёт. Пример очень простой, есть сегменты данных, защищенные от записи (тупо ROM, 4ex) , как сегменты кода. Переменная может оказаться в таком сегменте, ибо вызов функции изменения, судя по сигнатуре не предполагает.

Переменная может оказаться в таком сегменте

Я настолько глубоко не копал, но подозреваю, что компилятор просто не даст такое сделать

Строки, которые прописываются в строковых литералах (let s = "Hello, World!";) и предположительно будут помещены компилятором в сегмент данных, имеют другой тип &str — то есть ссылка на кусок строки без мутабельности (а точнее &'static str, что означает, что эта ссылка остаётся валидной в течение всей жизни программы)

Ну предположительно, это тонкий лед =)

Но в этом я проблемы большой не вижу - запретить каст без ансейфа легко.

А вот консуминг переменных это очень большая проблема, как по мне. Ещё подумаю, может это сделано для однотипности обычных и ссылочных параметров.

Ну или нужно придумать пример, где это небезопасно, вызов без передачи владения.

Вообще то создаёт. Пример очень простой, есть сегменты данных, защищенные от записи (тупо ROM, 4ex) , как сегменты кода.

Не может локальная переменная оказаться в сегменте ROM (иначе как через особо хитрую глобальную оптимизацию с анализом всей программы - но такая оптимизация и присваивание let mut v = v; увидит).

Оказаться в этом сегменте может только статическая неизменяемая переменная, либо литерал (выступающий в роли анонимной статической переменной), но владение ими вы никуда передать и не сможете (потому что у вас его нет).

Это принцип работы borrow checker. Если владение будет передаваться неявно, то для компилятора может стать проблемой вычислять какой области видимости принадлежит переменная особенно в async. Если хочется добиться логики которую вы описали, то следует явно передавать ссылки на значения:

fn main() {
    let mut c = "str".to_string();
    f1(&mut c);
    f2(c);
}


fn f1(c: &mut String) {}
fn f2(c: String) {}

Нет, я не хочу мутабельности.

Я спрашиваю, почему вдруг потребляется иммутабельный Параметр?

Если это небезопасно, то нужен пример, в каком случае.

@andreymal привел отличные примеры. Также могут быть вызваны memory leaks. Если наша функция аллоцирует память, но раз мы не освобождаем s, то логично, что не должны освобождать и другие переменные. А чекать, что s пришла извне и ее надо сохранить, а остальные не следует, может быть довольно затратно особенно когда мы подключаем async, пойнтеры и ТД. Плюс мы не сможем так просто параллелить компиляцию и как будто в таком случае проще воспользоваться GC-языками. Ну и хочется явно видеть, что функция делает.

Очень невнятный поток сознания.

Раст прекрасно видит область жизни переменной, вот в конце этой области и надо её удалять. А не при вызове подфункции.

При передаче владения в подфункцию область жизни перемещается в эту самую подфункцию, вот и удаляется в конце области подфункции

Это мы уже обсудили.

Нет только понятия причин такого дизайна. Возможно, где то на родном сайте есть мнение от создателей.

Может, потому что делать иначе просто нет смысла? Для немутабельной передачи с сохранением жизни исходной переменной уже есть передача по ссылке. А владение с запретом мутабельности для меня звучит как-то противоречиво, это уже как будто и не владение совсем

ИМХО, мутабельность/иммутабельность ссылок в расте — вводящий в заблуждение концепт. Потому что возможность менять значение не всегда совпадает с уникальностью ссылки. На эту тему есть несколько постов на английском, но я их сейчас сходу не найду.

Например, часто встречается interiour mutability (доступный через примитв UnsafeCell), который позволяет менять объект, если есть какая-то гарантия, что никто больше его не меняет. Например, Mutex берет блокировку, а Cell запрещает передавать себя между потоками.

Поэтому осмысленнее иметь не мутабельные/иммутабельные ссылки, а уникальные/неуникальные. Тогда получается три самых распространенных способа передавать объекты (если не считать Pin'ы):

  • владение (`T`) — объект принадлежит тебе полностью и исключительно; никаких ссылок на него не существует и теперь это твоя ответственность освободить память.

  • Shared-ссылка (`&T`) — объект где-то есть, и твоя ссылка на него не уникальна; возможно его кто-то может читать из других потоков.

  • Unique-ссылка (`&mut T`) — объект где-то есть, но кроме тебя его никто не читает и это единственная активная ссылка на него. Часто это позволяет менять объект.

Тогда общая картина становится немного более логичной. Владение позволяет брать на объект ссылки (в рамках borrow checker'a) и делает тебя ответственным за освобождение ресурсов. А вот уже ссылки позволяют получать доступ к объекту. Так у нас владение и ссылки становятся ортогональными концептами с непересекающимися фичами.

Из факта владения объектом часто следует возможность взять &mut ссылку (или обычную), но не всегда borrow checker это разрешит. Например, если вы передали &Vec<T> в функцию и получили в ответ ссылку на один его элемент, то теперь вы не можете взять и добавить элемент в вектор, хотя им владеете и несёте ответственность освободить память. Ведь вдруг при вставке вектор переаллоцируется и указатель сломается. Borrow checker не даст создать уникальную ссылку на Vec, пока существует какая-то другая ссылка на него же. Таким образом, владение объектом не гарантирует возможность доступа к нему; чтобы взаимодействовать с данными нужно создавать соответствующие ссылки.

А мутабельность owned значений (`let mut x: T`) просто синтаксическая соль, которая на самом деле ни на что не влияет в большом смысле.

Потребляется он потому, что программист так написал. Чтобы не потреблялся, надо передавать по ссылке. Кроме мутабельной ссылки есть ещё и иммутабельная:

fn f3(c: &String) {}

Почему переменная сгорает (потребляется).

Потому что так написана функция my_func. Написали бы её принимающей &str - ничего бы не сгорало.

Почему все статьи нахваливающие Раст статьи никогда не приводят реальных примеров, реальных задач с реальными структурами данных, где Раст показывает свои сильные стороны.

Покажите как в Расте делается иерархия вложенных виджетов, которые шарят общие стили и связываются ссылками между собой и с моделью/контроллером. Как декларации времени жизни предотвращает утечки памяти. Как borrow checker обеспечивает безопасность памяти.

Если GUI-виджеты не ваша тема, покажите эти замечательные механизмы на примере бизнес-объектов, офисных документов, медиа-данных или игровых движков.

Насколько я увидел из примеров и новостей, Раст не просто "делает программистам больно", он ничего за это не дает - Раст-программы так же текут и падают на сложных задачах.

Простите, а где Вы увидели «из примеров и новостей», что «Раст-программы так же текут и падают на сложных задачах»?

Приведите, пожалуйста, конкретные ссылки.

Пожалуйста:

Надеюсь на взаимность, хотелось бы увидеть пример (в виде комментария или статьи), как Раст помогает проектировать структуры данных любой предметной области, свободные от утечек, крашей и data races.

Говорить про «утечки и краши» языка, и привести как аргумент ссылку на форум, где обсуждается «unwrap() hurts readability» …

Запомните уже наконец: memory safety - это не про гарантии отсутствия «утечек», это про гарантии отсутствия UB при некорректной работе с той самой памятью, которое легко допустимо в С/C++, например. И (safe) rust дает эти самые гарантии.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации