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++, только компилятор проверяет, что вы им не злоупотребили.
Чтобы дальше не путаться, нужно один раз увидеть, что физически лежит в переменной.
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». Воевать не с чем: правила — не аксиомы, а следствия одной модели.
Модель. В каждый момент времени для каждого значения существует ровно один путь его освобождения, и компилятор знает, где этот путь заканчивается. Ссылки — временные разрешения посмотреть на значение или изменить его, действующие строго внутри жизни владельца.
Из модели механически выводятся правила, которые вы увидите в документации:
- У каждого значения ровно один владелец. Присваивание или передача в функцию перемещает владение, если тип не реализует
Copy. - Владелец выходит из области видимости — значение уничтожается, ровно один раз.
- Одновременно можно иметь либо любое число разделяемых ссылок
&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».
Где в компиляторе живут эти проверки
Полезно один раз увидеть конвейер целиком: тогда становится понятно, почему одни ошибки приходят раньше других и почему сборка не бывает мгновенной.
каталог src"] --> LEX["Лексер и парсер
токены, AST"] LEX --> EXP["Раскрытие макросов
macro_rules и proc-macro"] EXP --> HIR["HIR
разрешение имён, вывод
и проверка типов, трейты"] HIR --> MIR["MIR
упрощённый граф
потока управления"] MIR --> BC["Borrow checker
владение, заимствования, NLL"] BC --> MONO["Мономорфизация обобщений
и оптимизации MIR"] MONO --> LLVM["LLVM IR
оптимизации и машинный код"] LLVM --> LINK["Компоновка
rlib, бинарь, статический std"] BC -->|"E0382, E0499, E0502 —
дальше конвейер не идёт"| ERR["Сборки нет"]
Ключевая деталь — borrow checker работает на MIR, после проверки типов и до генерации кода. Он ничего не добавляет в машинный код: это чистая статическая проверка, стоимость которой — секунды сборки, а не наносекунды исполнения. Формально устройство этой проверки исследовано академически: работа RustBelt (Jung et al., POPL 2018) доказывает корректность модели владения и безопасность ряда библиотек стандартной библиотеки, использующих unsafe внутри. Это редкий для промышленного языка случай, когда «безопасность» — не маркетинг, а теорема с оговорёнными условиями. Как устроены такие проверки вообще — в треке «Компиляторы», статьи «Проверка типов» и «Промежуточное представление»; чем это отличается от подхода со сборщиком мусора — в «Сборка мусора».
Читать сообщения компилятора — половина обучения Rust
Rust — язык, в котором вы будете разговаривать с компилятором каждый день. Хорошая новость: диагностика 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. Вместо null — Option<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; чем дальше — тем выше шанс, что вы платите за гарантии, которые вам не нужны.
Дорожная карта курса
Курс из четырнадцати статей ведёт от установки тулчейна до продового сервиса. Порядок не случаен: владение → времена жизни → трейты → ошибки — это одна лестница, где каждая ступень стоит на предыдущей.
- Установка и инструментарий —
rustup, тулчейны и цели,cargoкак единая точка входа,clippy,rustfmt, настройка редактора. - Основы Rust — типы, неизменяемость по умолчанию, выражения, сопоставление с образцом, функции и модули.
- Владение и заимствование — сердце языка: перемещение,
Copy, ссылки, срезы,RcиRefCellи цена каждого варианта. - Времена жизни — зачем нужны аннотации, правила элизии, как читать
'aв чужом коде и разбор типичных ошибок. - Типы и трейты — обобщения, статическая и динамическая диспетчеризация, ключевые трейты стандартной библиотеки.
- Обработка ошибок —
ResultиOption, оператор?, свои типы ошибок,thiserrorиanyhow, когда паника уместна. - Коллекции и итераторы — что выбрать под задачу, ленивость, адаптеры, аллокации и эффективность.
- Бесстрашная конкурентность — потоки, каналы,
Send/Sync,Arc<Mutex<T>>, атомики и разделяемое состояние. - Асинхронный Rust —
Futureкак конечный автомат,tokio,async/awaitи его подводные камни. - unsafe и FFI — когда без него нельзя, как ограничить, связь с C, проверка через Miri.
- Тестирование и документация — unit- и интеграционные тесты, doc-тесты, property-based, фаззинг, бенчмарки.
- Прод на Rust — структура проекта и workspace, модули, конфигурация, логи и ошибки в проде.
- Экосистема и ресурсы — веб, встраиваемое, 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, и умение это сказать входит в профессию.
Источники
- Официальное: The Rust Programming Language, Rust by Example, The Rust Reference, The Cargo Book, Error Index, Edition Guide, документация std
- Книги: Jim Blandy et al., Programming Rust, 2nd ed. (O’Reilly); Jon Gjengset, Rust for Rustaceans; Mara Bos, Rust Atomics and Locks; Luca Palmieri, Zero To Production In Rust
- Про безопасность памяти: MSRC, A proactive approach to more secure code, Chromium Memory Safety, Google, Eliminating Memory Safety Vulnerabilities at the Source, CISA, The Case for Memory Safe Roadmaps
- Теория и инструменты: RustBelt, POPL 2018, Miri, Ferrocene, Rust Forge, rust-lang/rfcs
- Практика и кейсы: rustlings, Rust Playground, Rust for Linux, Cloudflare Pingora, Discord: from Go to Rust, This Week in Rust
Что дальше
Установка и инструментарий: rustup, cargo, clippy, rustfmt — соберём рабочее окружение: тулчейны и цели, cargo как единая точка входа для сборки, тестов и документации, линтер и форматтер в CI, настройка редактора так, чтобы сообщения компилятора приходили до нажатия «собрать».