Rust Rust: обзор языка, философия безопасности и дорожная карта курса
0%

Rust: обзор языка, философия безопасности и дорожная карта курса

Rust: обзор языка, философия безопасности и дорожная карта курса

Почти каждый язык программирования выбирает две вещи из трёх: контроль над памятью, безопасность и отсутствие тяжёлого рантайма. C и C++ берут контроль и отсутствие рантайма, а безопасность оставляют на совести программиста. Java, C#, Go, Python и Elixir берут безопасность и отсутствие ручного управления памятью, платя за это сборщиком мусора, паузами и потерей контроля над раскладкой данных. Rust — попытка получить все три, перенеся проверки из времени исполнения во время компиляции.

Это единственная фраза, которую нужно запомнить перед курсом. Всё остальное — владение, заимствование, времена жизни, трейты Send и Sync, Result вместо исключений — следствия одного решения: если гарантию нельзя проверить бесплатно во время работы программы, её нужно проверить один раз при сборке, а в машинный код не класть ничего.

Зачем это вообще понадобилось

Проблема, которую решает Rust, измерена и оцифрована. Microsoft Security Response Center разобрал свои уязвимости за 12 лет и получил: около 70 % CVE — это ошибки работы с памятью. Команда Chromium независимо пришла к той же цифре — 70 % серьёзных багов безопасности. Речь не про экзотику, а про один и тот же короткий список: обращение к освобождённой памяти, двойное освобождение, выход за границу буфера, разыменование нулевого указателя, гонка данных.

Обратный эксперимент поставил Google в Android. Новый код там постепенно писали на безопасных языках, старый не переписывали — и доля уязвимостей памяти упала с 76 % в 2019 году до 24 % в 2024-м. Вывод из этого эксперимента важнее самого Rust: класс ошибок закрывается не аккуратностью и не ревью, а инструментом, который делает ошибку невыразимой. С 2023 года это стало и позицией регуляторов — см. «The Case for Memory Safe Roadmaps» CISA и партнёрских агентств.

Что именно скрывается за каждым из этих классов ошибок, как они выглядят в C и почему компилятор C не может их поймать — разбирается в треке «Системное программирование», прежде всего в статьях «Память и указатели», «Динамическая память» и «Неопределённое поведение». Если вы приходите из C или C++, начните с них: Rust читается совсем иначе, когда помнишь, от чего именно он защищает.

Модель исполнения: где живут данные и кто их освобождает

В Rust нет сборщика мусора и нет виртуальной машины. Скомпилированная программа — обычный нативный бинарник, который стартует за миллисекунды и не имеет фонового потока, решающего, когда собрать мусор. Освобождение памяти детерминировано: оно происходит в точке, известной на этапе компиляции, — там, где владелец значения выходит из области видимости. Это тот же приём, что RAII в C++, только компилятор проверяет, что вы им не злоупотребили.

Чтобы дальше не путаться, нужно один раз увидеть, что физически лежит в переменной.

Раскладка значений Rust в стеке и куче: String как тройка ptr/len/cap и срез как толстый указатель

use std::mem::size_of;

fn main() {
    let n: i32 = 42;                    // всё значение — 4 байта на стеке
    let s: String = String::from("Rust"); // 24 байта на стеке + буфер в куче
    let part: &str = &s[0..2];          // 16 байт: адрес и длина, владения нет

    println!("i32={} String={} &str={}", size_of::<i32>(), size_of::<String>(), size_of::<&str>());
    println!("{n} {s} {part}");
}
i32=4 String=24 &str=16
42 Rust Ru

Отсюда сразу три вывода, на которых держится весь язык. Первое: у буфера в куче есть ровно один владелец — переменная s; когда она уходит, вызывается Drop::drop и память возвращается аллокатору. Второе: копирование s в другую переменную скопировало бы 24 байта заголовка, и тогда владельцев одного буфера стало бы двое — а это двойное освобождение; поэтому Rust вместо копирования делает перемещение (move) и объявляет исходную переменную недоступной. Третье: part не владеет ничем и не должен пережить s — иначе получим указатель в освобождённую память.

Все три вывода компилятор умеет проверять. Именно эта проверка и называется владением и заимствованием.

Владение — это модель, а не список запретов

Новички обычно учат владение как три правила из документации и потом «воюют с borrow checker». Воевать не с чем: правила — не аксиомы, а следствия одной модели.

Модель. В каждый момент времени для каждого значения существует ровно один путь его освобождения, и компилятор знает, где этот путь заканчивается. Ссылки — временные разрешения посмотреть на значение или изменить его, действующие строго внутри жизни владельца.

Из модели механически выводятся правила, которые вы увидите в документации:

  1. У каждого значения ровно один владелец. Присваивание или передача в функцию перемещает владение, если тип не реализует Copy.
  2. Владелец выходит из области видимости — значение уничтожается, ровно один раз.
  3. Одновременно можно иметь либо любое число разделяемых ссылок &T (только чтение), либо ровно одну исключительную &mut T (чтение и запись), но не то и другое сразу.

Третье правило часто формулируют как «одна запись XOR много чтений». Его смысл проще, чем кажется: нельзя одновременно наблюдать значение и менять его. Из этого одного условия следуют и отсутствие гонок данных, и невозможность инвалидации итератора, и корректность оптимизаций компилятора.

Обратите внимание на переходы «последнее использование ссылки». До 2018 года заимствование жило до конца блока, и компилятор отвергал очевидно корректный код. Механизм NLL (non-lexical lifetimes) привязал время жизни заимствования к последней точке использования — с тех пор большая часть исторических жалоб на borrow checker неактуальна, и если вы читаете старую статью про «борьбу с компилятором», сверяйтесь с датой.

Ошибка первая: использование после перемещения

fn main() {
    let s = String::from("Rust");
    let t = s;              // владение перемещено в t
    println!("{s}");        // s больше не владеет ничем
    println!("{t}");
}
error[E0382]: borrow of moved value: `s`
 --> src/main.rs:4:15
  |
2 |     let s = String::from("Rust");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let t = s;
  |             - value moved here
4 |     println!("{s}");
  |               ^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let t = s.clone();
  |              ++++++++

В C++ этот код собрался бы и молча работал: t скопировал бы указатель, оба объекта попытались бы освободить один буфер, программа падала бы раз в неделю в проде. Здесь ошибка возникает при сборке, и компилятор сразу предлагает две трактовки: если копия нужна — clone(), если не нужна — передавайте ссылку.

Ошибка вторая: инвалидация при изменении

fn main() {
    let mut v = vec![10, 20, 30];
    let first = &v[0];      // разделяемая ссылка внутрь буфера
    v.push(40);             // push может перевыделить буфер и переехать
    println!("{first}");    // ссылка указывала бы в старый адрес
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let first = &v[0];
  |                  - immutable borrow occurs here
4 |     v.push(40);
  |     ^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{first}");
  |               ------- immutable borrow later used here

Это тот самый баг, который в C++ выглядит как «итератор протух», в Java — как ConcurrentModificationException во время работы, а в Go — как молча прочитанный старый массив. Обратите внимание, что компилятор не рассуждает о том, случится ли перевыделение на самом деле: push берёт &mut self, а исключительная ссылка не может сосуществовать с разделяемой. Проверка структурная, а не эвристическая — поэтому она не даёт ложноотрицательных срабатываний.

Ошибка третья: висячая ссылка

fn dangle() -> &String {
    let s = String::from("Rust");
    &s                       // s умрёт на выходе, ссылка станет висячей
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:16
  |
1 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there are no values for it
          to be borrowed from

Формулировка «нет значения, из которого можно было бы позаимствовать» и есть суть времён жизни: аннотация 'a не продлевает жизнь данным, а описывает связь «результат живёт не дольше, чем аргумент». В 99 % случаев компилятор выводит эту связь сам, и аннотации вы пишете редко.

Чем за это платят

Честно: borrow checker отвергает часть корректных программ. Двусвязный список, граф с обратными ссылками, наблюдатель, держащий ссылку на субъект, — всё это не выражается напрямую. Ответ Rust не «так не делайте», а «скажите явно, какой ценой»: подсчёт ссылок Rc/Arc, проверка правила XOR во время исполнения через RefCell/Mutex, арены и индексы вместо указателей, в крайнем случае — unsafe с ручным доказательством инвариантов. Разбор этих вариантов — в статьях «Владение и заимствование» и «unsafe и FFI», а классическое упражнение на эту тему — «Learn Rust With Entirely Too Many Linked Lists».

Где в компиляторе живут эти проверки

Полезно один раз увидеть конвейер целиком: тогда становится понятно, почему одни ошибки приходят раньше других и почему сборка не бывает мгновенной.

Ключевая деталь — borrow checker работает на MIR, после проверки типов и до генерации кода. Он ничего не добавляет в машинный код: это чистая статическая проверка, стоимость которой — секунды сборки, а не наносекунды исполнения. Формально устройство этой проверки исследовано академически: работа RustBelt (Jung et al., POPL 2018) доказывает корректность модели владения и безопасность ряда библиотек стандартной библиотеки, использующих unsafe внутри. Это редкий для промышленного языка случай, когда «безопасность» — не маркетинг, а теорема с оговорёнными условиями. Как устроены такие проверки вообще — в треке «Компиляторы», статьи «Проверка типов» и «Промежуточное представление»; чем это отличается от подхода со сборщиком мусора — в «Сборка мусора».

Читать сообщения компилятора — половина обучения Rust

Rust — язык, в котором вы будете разговаривать с компилятором каждый день. Хорошая новость: диагностика rustc считается образцовой в индустрии — она показывает не только место ошибки, но и источник конфликта, и обычно готовое исправление.

Анатомия сообщения об ошибке rustc с выносками: код ошибки, координаты, источник конфликта, каретки и последнее использование ссылки

Практические привычки, экономящие месяцы:

  • Читайте сообщение с конца. Блоки help: и note: идут после текста ошибки и часто содержат готовую правку со знаками + прямо в коде. Три четверти ситуаций закрываются, не доходя до документации.
  • rustc --explain E0502 печатает страницу с объяснением класса ошибки и минимальным примером. Полный каталог — Rust Error Index.
  • Не правьте по первому впечатлению. Типичная ошибка новичка — увидеть E0382 и расставить .clone() везде. Иногда это верный ответ (клонирование строки на холодном пути дешевле часа размышлений), но чаще правильный вопрос — «а нужно ли мне здесь владение или хватит &».
  • Включите clippy сразу. cargo clippy печатает в том же формате, ловит неидиоматичный код и умеет чинить его сам: cargo clippy --fix. Настройка — в статье «Установка и инструментарий».

Что Rust гарантирует, а что нет

Самый вредный миф про Rust — «если скомпилировалось, значит работает». Компилятор закрывает конкретный, хотя и очень дорогой, список классов ошибок. Остальное — по-прежнему ваша работа.

Гарантировано в безопасном (safe) коде:

Класс ошибки Как закрыт
Обращение к освобождённой памяти (use-after-free) Ссылка не может пережить владельца
Двойное освобождение Владелец ровно один, Drop вызывается один раз
Выход за границу буфера Проверка индекса; при выходе — паника, не порча памяти
Разыменование null Нет null; отсутствие значения выражается через Option<T>
Чтение неинициализированной памяти Переменную нельзя прочитать до инициализации
Гонка данных (data race) Трейты Send и Sync проверяются на этапе компиляции
Забытая ветка switch match обязан быть исчерпывающим
Молча проигнорированная ошибка Result помечен #[must_use]

Не гарантировано, и это надо знать наизусть:

  • Логические ошибки. Скидка считается по неверной формуле — компилятор не в курсе.
  • Паники. unwrap() на None, выход за границу среза, деление на ноль, явный panic!. Программа падает контролируемо, память не портится, но сервис отвечает пятисоткой.
  • Переполнение целых. В debug-сборке — паника, в release по умолчанию — обёртка по модулю. Не UB, но и не то, что вы имели в виду.
  • Взаимоблокировки. Гонки данных исключены, дедлоки — нет. Пример ниже собирается без единого предупреждения.
  • Утечки памяти. Цикл из Rc, забытый JoinHandle, std::mem::forget — всё это безопасно с точки зрения языка. Утечка не нарушает безопасность памяти, она просто утечка.
  • Всё, что внутри unsafe, а также любой вызов через FFI. Там гарантии переходят к вам.
use std::sync::Mutex;
use std::thread;

static A: Mutex<i32> = Mutex::new(0);
static B: Mutex<i32> = Mutex::new(0);

fn main() {
    let t = thread::spawn(|| {
        let _a = A.lock().unwrap();
        let _b = B.lock().unwrap();   // порядок захвата: A, потом B
    });
    let _b = B.lock().unwrap();
    let _a = A.lock().unwrap();       // порядок захвата: B, потом A — классический дедлок
    t.join().unwrap();
}

Этот код компилируется, проходит clippy и однажды ночью повиснет. Значит, дисциплина порядка захвата, таймауты и тесты никуда не делись — см. «Бесстрашная конкурентность» и параллельный разбор тех же граблей на C в «Потоки и синхронизация».

И то же самое про паники — вот как выглядит выход за границу:

thread 'main' panicked at src/main.rs:3:20:
index out of bounds: the len is 0 but the index is 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

Четыре опоры, кроме владения

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

1. Алгебраические типы и исчерпывающий match. Вместо nullOption<T>, вместо исключений — Result<T, E>. Тони Хоар назвал изобретение null «ошибкой на миллиард долларов»; Rust просто не даёт его выразить. Добавили вариант в перечисление — код не соберётся, пока вы не обработаете его во всех match.

2. Ошибки как значения. Result — обычный тип, оператор ? пробрасывает ошибку вверх, #[must_use] не даёт молча выбросить. Отсюда предсказуемый поток управления: нет невидимых точек выхода, как у исключений. Подробно — в «Обработка ошибок».

3. Трейты вместо наследования. Поведение описывается трейтами, обобщения по умолчанию компилируются в конкретные копии (мономорфизация) — вызов статический и инлайнится. Динамическая диспетчеризация есть, но она явная: Box<dyn Trait>. См. «Типы и трейты».

4. Конкурентность, проверенная типами. Два маркерных трейта — Send (значение можно передать в другой поток) и Sync (на значение можно ссылаться из нескольких потоков) — превращают «не делайте так» из документации в ошибку компиляции:

use std::rc::Rc;
use std::thread;

fn main() {
    let counter = Rc::new(0);
    let c = Rc::clone(&counter);
    thread::spawn(move || println!("{c}"));   // Rc не атомарен, значит не Send
}
error[E0277]: `Rc<i32>` cannot be sent between threads safely
 --> src/main.rs:7:19
  |
7 |     thread::spawn(move || println!("{c}"));
  |     ------------- ^ `Rc<i32>` cannot be sent between threads safely
  |     |
  |     required by a bound introduced by this call
  |
  = help: the trait `Send` is not implemented for `Rc<i32>`
  = note: required for the closure to implement `Send`
note: required by a bound in `std::thread::spawn`

(вывод немного сокращён). Компилятор сообщил вам то, что в C++ или Python вы узнали бы из падения раз в тысячу запусков: счётчик ссылок Rc неатомарный, для потоков нужен Arc. Отсюда лозунг fearless concurrency: не «параллелизм стал простым», а «целый класс ошибок параллелизма перестал компилироваться».

Абстракции без накладных расходов

Второй лозунг — zero-cost abstractions: «то, чем вы не пользуетесь, ничего не стоит; то, чем пользуетесь, нельзя написать вручную лучше». Практически это значит, что итераторный конвейер компилируется в тот же цикл, что вы написали бы руками:

/// Сумма чётных чисел. С -O компилируется в тот же машинный код,
/// что и ручной цикл, и векторизуется LLVM.
pub fn sum_even(xs: &[u64]) -> u64 {
    xs.iter().filter(|x| *x % 2 == 0).sum()
}

Проверяется это не верой, а глазами: Compiler Explorer или cargo asm. Но у бесплатности есть границы, и о них честнее знать заранее:

  • Мономорфизация копирует код обобщённой функции под каждый конкретный тип. Это быстро на исполнении и дорого по времени сборки и размеру бинаря.
  • Box<dyn Trait> — это косвенный вызов через таблицу, инлайнинга нет. Осознанный обмен размера кода на гибкость.
  • async превращает функцию в конечный автомат; это дёшево, но не бесплатно, и добавляет свой класс проблем — см. «Асинхронный Rust».
  • Проверки границ обычно устраняются оптимизатором на итераторах и остаются при ручной индексации в цикле. Это одна из причин, по которой идиоматичный Rust с итераторами часто быстрее «оптимизированного вручную».

Ещё важнее общее место: Rust даёт контроль над раскладкой данных в памяти — а на современных процессорах именно локальность, а не число инструкций, определяет скорость. Почему так, разбирается в «Кеш и локальность» и «Память». И обязательная оговорка: переписывание на Rust без замеров — самый дорогой способ ошибиться; методика измерения — в «Честный бенчмаркинг».

Как выглядит идиоматичный код

Небольшая законченная программа: топ-3 слова текста. Ничего специфически «системного», зато видны сразу четыре черты языка.

use std::collections::HashMap;

/// Возвращает n самых частых слов. Заметьте: результат заимствует
/// строки из text — ни одной новой аллокации под слова здесь нет,
/// а компилятор гарантирует, что text переживёт результат.
fn top_words(text: &str, n: usize) -> Vec<(&str, usize)> {
    let mut counts: HashMap<&str, usize> = HashMap::new();

    for raw in text.split_whitespace() {
        // Обрезаем пунктуацию по краям слова
        let word = raw.trim_matches(|c: char| !c.is_alphanumeric());
        if word.is_empty() {
            continue;
        }
        // entry API: одна операция вместо «поискать, потом вставить»
        *counts.entry(word).or_insert(0) += 1;
    }

    let mut sorted: Vec<(&str, usize)> = counts.into_iter().collect();
    // По убыванию частоты, при равенстве — по алфавиту: порядок детерминирован
    sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
    sorted.truncate(n);
    sorted
}

fn main() {
    let text = "мы пишем код, код, код и читаем ошибки — ошибки!";
    for (word, count) in top_words(text, 3) {
        println!("{word}: {count}");
    }
}
код: 3
ошибки: 2
и: 1

Что здесь стоит заметить. Тип возврата Vec<(&str, usize)> заимствует из аргумента: слова не копируются, а компилятор сам вывел, что результат не может пережить text. HashMap берётся из стандартной библиотеки, entry избавляет от двойного поиска, итераторы и sort_by — обычный идиоматичный код, который вы будете писать каждый день. И ни одной строчки про освобождение памяти: counts уничтожится на выходе из функции, буферы вернутся аллокатору.

Философия проекта: стабильность без застоя

Rust — не только язык, но и процесс, и его свойства влияют на вашу работу сильнее, чем кажется.

  • Стабильность как обещание. Код, собиравшийся на 1.0, собирается сегодня. Новые релизы выходят строго раз в шесть недель по трём каналам — stable, beta, nightly; порядок описан на Rust Forge.
  • Редакции (editions) вместо версий языка. Раз в три года выходит edition (2015, 2018, 2021, 2024), где можно менять синтаксис и умолчания несовместимо. Ключевое: редакция — свойство крейта, а не всей программы; крейты разных редакций собираются в одном бинаре. Это не форк языка, а способ эволюционировать, не ломая экосистему — см. Edition Guide.
  • Изменения идут через RFC. Любая фича проходит публичное обсуждение в rust-lang/rfcs, потом реализацию под флагом в nightly, потом стабилизацию. Медленно и предсказуемо.
  • Управление вынесено в фонд. Rust Foundation с 2021 года; спонсоры — AWS, Google, Microsoft, Huawei и другие. Язык больше не зависит от одной компании.
  • Инструменты для доказательства, а не только для сборки. Miri интерпретирует код и ловит UB внутри unsafe; для регулируемых отраслей есть сертифицированный тулчейн Ferrocene (ISO 26262, IEC 61508).

Честно про цену

Rust не бесплатен, и разговор о его внедрении начинается именно здесь.

Время сборки. Это самая частая жалоба в ежегодных опросах сообщества. Причин несколько: мономорфизация плодит код, LLVM оптимизирует его долго, единицей компиляции является крейт целиком, а процедурные макросы (serde, sqlx) исполняются во время сборки. Ориентиры для среднего веб-сервиса на tokio + axum + sqlx: холодная сборка релиза — единицы минут, инкрементальная отладочная — секунды или десятки секунд. Смягчается это набором приёмов: cargo check вместо cargo build в цикле правок, разбиение на несколько крейтов в workspace, быстрый компоновщик (lld, mold), кеш sccache, отключение лишних фич зависимостей. Разбор — в статьях «Установка и инструментарий» и «Прод на Rust».

Кривая обучения. Реалистичный ориентир для опытного разработчика: неделя до первой рабочей программы, месяц до спокойного чтения ошибок borrow checker, три-шесть месяцев до уверенного проектирования API с обобщениями и временами жизни. Тяжелее всего приходят из языков со сборкой мусора: там модель памяти была невидимой, и её приходится строить с нуля. Приходящим из C и C++ проще — они уже знают, чего нельзя, просто раньше за этим следили сами.

Экосистема тонкая по краям. Стандартная библиотека намеренно мала: в ней нет HTTP-клиента, асинхронного рантайма, сериализации, генератора случайных чисел. Всё это — сторонние крейты, и выбор зависимостей становится архитектурным решением с первого дня. Карта экосистемы и критерии выбора — в «Экосистеме и ресурсах».

Найм и команда. Разработчиков на Rust меньше, чем на Java или Python, и стоят они дороже. Если в команде один энтузиаст, а остальные не хотят учиться — сервис на Rust станет технологическим долгом, каким бы быстрым он ни был.

Куда Rust тащить не стоит. Прототип гипотезы на выброс; внутренняя админка; glue-скрипты; типичный CRUD поверх Postgres, где узкое место — сеть и база, а не CPU; исследовательский код в data science, где библиотечная экосистема Python на порядок богаче. В этих сценариях вы заплатите временем сборки и обучением, не получив ничего взамен. Детальная матрица «брать или не брать» — в «Прод на Rust»; сравнение с соседями по нише — в «Современных альтернативах C», а если критичны простота и скорость найма, честной альтернативой часто оказывается Go.

Где Rust уже победил

Не абстрактные обещания, а системы, которые работают прямо сейчас: драйверы в ядре Linux начиная с 6.1 (Rust for Linux), компоненты Android и ядра Windows, движок стилей и рендеринга Firefox, Firecracker — микровиртуализация, на которой работает AWS Lambda, Pingora от Cloudflare, обрабатывающий триллионы запросов, сервис Read States в Discord, переписанный с Go ради предсказуемых хвостов задержек. Плюс целый пласт инструментов, которыми вы, возможно, уже пользуетесь: ripgrep, fd, bat, uv, ruff, Deno, Polars, TiKV, Vector.

Общий знаменатель у этих кейсов один: цена ошибки высока, а нагрузка на CPU или требования к хвостовым задержкам делают паузы сборщика мусора неприемлемыми. Это и есть ниша языка. Чем ближе задача к этому описанию, тем выгоднее Rust; чем дальше — тем выше шанс, что вы платите за гарантии, которые вам не нужны.

Дорожная карта курса

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

  1. Установка и инструментарийrustup, тулчейны и цели, cargo как единая точка входа, clippy, rustfmt, настройка редактора.
  2. Основы Rust — типы, неизменяемость по умолчанию, выражения, сопоставление с образцом, функции и модули.
  3. Владение и заимствование — сердце языка: перемещение, Copy, ссылки, срезы, Rc и RefCell и цена каждого варианта.
  4. Времена жизни — зачем нужны аннотации, правила элизии, как читать 'a в чужом коде и разбор типичных ошибок.
  5. Типы и трейты — обобщения, статическая и динамическая диспетчеризация, ключевые трейты стандартной библиотеки.
  6. Обработка ошибокResult и Option, оператор ?, свои типы ошибок, thiserror и anyhow, когда паника уместна.
  7. Коллекции и итераторы — что выбрать под задачу, ленивость, адаптеры, аллокации и эффективность.
  8. Бесстрашная конкурентность — потоки, каналы, Send/Sync, Arc<Mutex<T>>, атомики и разделяемое состояние.
  9. Асинхронный RustFuture как конечный автомат, tokio, async/await и его подводные камни.
  10. unsafe и FFI — когда без него нельзя, как ограничить, связь с C, проверка через Miri.
  11. Тестирование и документация — unit- и интеграционные тесты, doc-тесты, property-based, фаззинг, бенчмарки.
  12. Прод на Rust — структура проекта и workspace, модули, конфигурация, логи и ошибки в проде.
  13. Экосистема и ресурсы — веб, встраиваемое, WASM, выбор крейтов, куда двигаться дальше.

Соседние треки, которые сильно помогают: «Системное программирование» — что такое память и указатели без абстракций; «Операционные системы», особенно «Управление памятью» — как ядро реализует то, что Rust даёт вам в виде типов; «Производительность систем» — как доказывать, что стало быстрее.

Как проходить курс, чтобы уметь применять

  • Пишите руками. Держите открытым Rust Playground или локальный cargo new scratch. Каждый пример из статьи ломайте: уберите &, добавьте второй &mut, попробуйте вернуть ссылку на локальную переменную — сообщения компилятора и есть учебник.
  • Пройдите rustlings параллельно с первыми пятью статьями: сотня маленьких упражнений, которые не компилируются, пока вы их не почините.
  • Читайте «книгу». The Rust Programming Language — канонический бесплатный учебник; этот курс идёт плотнее и с прицелом на прод, но не заменяет её.
  • Не начинайте с async и unsafe. Оба даются легко только после владения и трейтов. Асинхронность в Rust — не «параллелизм для новичков», а отдельная модель со своими граблями.
  • Не оптимизируйте раньше времени. Идиоматичный код с итераторами почти всегда достаточно быстр; сначала правильно, потом измерение, и только потом оптимизация.
  • Включите строгость сразу: cargo clippy -- -D warnings в CI и #![deny(unsafe_code)] в крейтах, где unsafe не нужен. Пусть машина учит дисциплине.

Мини-словарь

Термины, которые встретятся уже в следующей статье:

  • Крейт (crate) — единица компиляции и распространения: библиотека или бинарник. Собирается целиком.
  • Cargo — сборщик, менеджер зависимостей и точка входа для тестов, документации и публикации.
  • Владение (ownership) — свойство переменной отвечать за освобождение значения.
  • Перемещение (move) — передача владения; исходная переменная становится недоступной.
  • Заимствование (borrow) — временная ссылка: &T для чтения, &mut T для изменения.
  • Время жизни (lifetime) — область кода, в течение которой ссылка гарантированно валидна.
  • Трейт (trait) — контракт поведения; ближайший аналог — интерфейс, но с обобщениями и реализациями по умолчанию.
  • Мономорфизация — генерация отдельной копии обобщённого кода под каждый конкретный тип.
  • NLL — правило, по которому заимствование заканчивается на последнем использовании ссылки.
  • MIR — внутреннее представление компилятора, на котором работает borrow checker.
  • unsafe — блок, где отключены четыре проверки компилятора и гарантии переходят к автору.
  • Edition — редакция языка (2015/2018/2021/2024), свойство отдельного крейта.

Мини-итог

  • Rust решает конкретную измеренную задачу: около 70 % серьёзных уязвимостей в больших C/C++-кодовых базах — это ошибки работы с памятью, и они закрываются не аккуратностью, а инструментом.
  • Владение и заимствование — не набор придирок, а модель: один владелец, детерминированное освобождение, ссылка не переживает владельца, «наблюдать и менять одновременно нельзя».
  • Проверки живут на MIR, на этапе компиляции, и ничего не стоят во время исполнения. Плата — секунды сборки и отвергнутая часть корректных программ, для которой есть явные обходные пути.
  • Компилятор закрывает use-after-free, двойное освобождение, выход за границы, null и гонки данных. Он не закрывает логические ошибки, паники, дедлоки и утечки.
  • Умение читать сообщения rustc — до help: и note:, с --explain под рукой — ускоряет обучение сильнее любого курса.
  • Цена реальна: минуты сборки, месяцы обучения, узкий рынок найма и тонкая по краям экосистема. Есть большой класс задач, где правильный ответ — не Rust, и умение это сказать входит в профессию.

Источники

Что дальше

Установка и инструментарий: rustup, cargo, clippy, rustfmt — соберём рабочее окружение: тулчейны и цели, cargo как единая точка входа для сборки, тестов и документации, линтер и форматтер в CI, настройка редактора так, чтобы сообщения компилятора приходили до нажатия «собрать».

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов