Кладовочка

О разном, чтобы было под рукой онлайн

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

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

Быстрый старт

Rust ставится через rustup — менеджер тулчейнов. Он же приносит cargo (сборка, зависимости, тесты) и rustc (сам компилятор). Отдельно ставить ничего не нужно.

# Linux / macOS — официальный установщик
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows: скачай rustup-init.exe с rustup.rs либо используй winget
winget install --id Rustlang.Rustup

# Проверка и обновление
rustc --version          # rustc 1.8x.x
cargo --version
rustup update            # обновить все тулчейны
rustup component add clippy rustfmt   # линтер и форматтер
rustup target add wasm32-unknown-unknown  # кросс-компиляция в WASM
cargo new hello          # новый бинарный проект в папке hello/
cargo new --lib mylib    # библиотека вместо бинарника
cd hello
cargo run                # собрать и запустить (debug)
cargo run --release      # оптимизированная сборка
cargo build              # только собрать
cargo check              # быстрая проверка типов без кодогенерации
cargo test               # запустить тесты
cargo add serde --features derive      # добавить зависимость в Cargo.toml
// src/main.rs — точка входа бинарного крейта
fn main() {
    let name = "Мир";
    println!("Привет, {name}!"); // Rust 1.58+: переменные подставляются прямо в строку
}

Структура проекта: Cargo.toml — манифест, src/main.rs — точка входа бинарника, src/lib.rs — корень библиотеки, src/bin/*.rs — дополнительные бинарники. Редакции языка (2015, 2018, 2021, 2024) задаются в манифесте и включают/выключают синтаксические изменения; для новых проектов бери edition = "2024".

Ключевые термины

Термин Значение
Владение (ownership) У каждого значения ровно один владелец; когда владелец выходит из области видимости, значение освобождается
Перемещение (move) Передача владения; старая переменная становится недоступной
Заимствование (borrow) Временная ссылка &T или &mut T — без передачи владения
Время жизни (lifetime) Область, в которой ссылка гарантированно валидна; в сигнатурах обозначается 'a
Крейт (crate) Единица компиляции: библиотека или исполняемый файл
Модуль (mod) Пространство имён внутри крейта; управляет видимостью через pub
Трейт (trait) Набор методов, описывающий поведение типа — аналог интерфейса
Trait object dyn Trait — динамическая диспетчеризация через указатель
Комбинатор Метод Option/Result/итератора, превращающий одно значение в другое без match
Zero-cost abstraction Абстракции, которые не добавляют накладных расходов в рантайме

Cargo: сборка, запуск и зависимости

Cargo — это одновременно система сборки, менеджер пакетов, раннер тестов и бенчмарков. Здесь же живут clippy (статический анализ) и rustfmt (форматирование). Правило простое: не собирай rustc вручную, всегда через Cargo.

# Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1", features = ["derive"] }   # фича derive включает кодогенерацию
tokio = { version = "1", features = ["full"] }      # асинхронный рантайм
anyhow = "1"

[dev-dependencies]
criterion = "0.5"        # зависимости только для тестов/бенчмарков

[features]
default = ["json"]       # фичи — условная компиляция
json = []
extra = ["dep:serde_json"]

[profile.release]
lto = true               # более агрессивные оптимизации релиза
codegen-units = 1
cargo check                       # самый быстрый цикл обратной связи
cargo build --release             # оптимизированная сборка
cargo run -- --port 8080          # аргументы после -- уходят в программу
cargo test -- --nocapture         # показать вывод println! в тестах
cargo test --doc                  # запустить примеры из доккомментариев
cargo fmt                         # отформатировать код
cargo clippy --all-targets -- -D warnings   # линт, ошибки делают сборку красной
cargo doc --open                  # сгенерировать документацию локально
cargo tree                        # дерево зависимостей
cargo update -p serde             # обновить конкретный пакет
cargo run --features extra        # собрать с необязательной фичей
# Workspace объединяет несколько крейтов в один target/ и общий lock-файл
[workspace]
members = ["crates/core", "crates/cli", "crates/web"]
resolver = "2"

[workspace.dependencies]          # общие версии зависимостей для всех членов
serde = { version = "1", features = ["derive"] }

Внутри члена workspace зависимость подключается как serde.workspace = true. Это убирает рассинхрон версий между крейтами. Сборка всего: cargo build --workspace, только одного: cargo build -p cli.

Полезные команды и подсказки

cargo new proj && cd proj
cargo watch -x test      # автозапуск тестов при изменении файлов (cargo-watch)
cargo expand             # показать код после раскрытия макросов (cargo-expand)
cargo audit              # проверить зависимости на известные уязвимости
cargo miri test          # поймать UB в unsafe-коде (nightly)
#![allow(unused)]           // внутренний атрибут — в самое начало файла или модуля

fn main() {
    let x = 5;
    dbg!(x * 2);            // печатает выражение, файл и строку → [src/main.rs:5] x * 2 = 10

    let _unused = 1;        // подчёркивание отключает warning о неиспользуемой переменной
}

Переменные, изменяемость, константы

По умолчанию переменные неизменяемы: чтобы менять значение, нужен явный mut. Это не косметика — компилятор использует неизменяемость для анализа заимствований и оптимизаций, а читателю кода снимает вопрос «а это точно где-то мутируется?».

let x = 5;
// x = 6;                     // ошибка: cannot assign twice to immutable variable

let mut y = 10;
y += x;                       // ok — y объявлена как mut

const MAX_POINTS: u32 = 100_000;   // константа: тип обязателен, значение — константное выражение
static GREETING: &str = "привет";  // статика: своя ячейка памяти, 'static-время жизни

// Затенение (shadowing): новая переменная с тем же именем, может менять тип
let spaces = "   ";
let spaces = spaces.len();    // &str → usize, обе версии существуют последовательно

Разница const и let: const подставляется на этапе компиляции и может быть объявлена в любой области (включая глобальную), static — одна ячейка памяти на программу. static mut требует unsafe, поэтому на практике вместо неё берут Mutex/OnceLock. Затенение — не мутация: старая переменная не меняется, просто новое имя перекрывает старое, и это позволяет переиспользовать имя без mut.

Типы данных: скаляры, кортежи, массивы, срезы

// Скаляры
let i: i32 = -42;             // знаковые: i8/i16/i32/i64/i128/isize
let u: u64 = 42;              // беззнаковые: u8/u16/u32/u64/u128/usize
let f: f64 = 3.14;            // f32 / f64
let b: bool = true;
let c: char = 'Я';            // 4 байта, Unicode-скаляр

// Составные
let t: (i32, f64, char) = (1, 2.0, 'z');
let (id, score, grade) = t;   // деструктуризация кортежа
let first = t.0;              // доступ по индексу

let arr: [i32; 3] = [1, 2, 3];      // массив: длина известна в compile-time
let zeros = [0u8; 16];              // 16 нулей
let sl: &[i32] = &arr[1..];         // срез — «вид» на часть массива или Vec

Приведение чисел делается через as (с усечением!), а безопасное — через TryFrom:

let big: i64 = 300;
let small = big as i8;                 // усечение без проверки — тип потеряет старшие биты
let ok: Result<u8, _> = u8::try_from(big);
assert!(ok.is_err());                  // 300 не влезает в u8 — ошибка вместо тихой порчи

let n = 255u8;
println!("{}", n.saturating_add(1));   // 255 — не переполняется
println!("{}", n.wrapping_add(1));     // 0   — циклический перенос
println!("{:?}", n.checked_add(1));    // None — явная проверка

В debug-сборке целочисленное переполнение паникует, в release — «заворачивается». Поэтому для ненадёжных данных всегда используй checked_*, saturating_* или wrapping_*, а не полагайся на режим сборки.

Строки: String и &str

В Rust две строки, и это главный источник путаницы у новичков. &str — заимствованный неизменяемый срез UTF-8-байтов (литералы, аргументы функций). String — владеющая, расширяемая строка в куче. Функции почти всегда принимают &str: это работает и с String благодаря deref coercion.

let s1: &str = "литерал";                  // живёт в статической памяти, не выделяется в куче
let s2: String = String::from("владеющая");
let s3 = String::with_capacity(64);        // сразу резервируем 64 байта — меньше реаллокаций

let concat = "привет, ".to_owned() + &s2;  // + конкатенирует String, второй операнд — &str
let via_format = format!("{s2} и {concat}"); // format! ничего не перемещает

let len_bytes = s2.len();                  // длина в БАЙТАХ
let len_chars = s2.chars().count();        // число Unicode-символов
let upper = s2.to_uppercase();
let trimmed = "  x  ".trim();
let parts: Vec<&str> = "a,b,c".split(',').collect();

// Индексация по байтам паникует, если попасть в середину UTF-8-символа:
let safe: &str = &s2[0..8];                // только если 8 — граница символа
fn print_len(s: &str) -> usize { s.len() }

let owned = String::from("привет");
print_len(&owned);            // &String → &str через deref coercion
print_len("привет");         // литерал уже &str

let v: Vec<String> = vec!["а".to_string(), "б".into()];
let joined = v.join(", ");    // "а, б" — join работает по &[String]

Для «либо заимствованной, либо владеющей» строки есть Cow<str>: use std::borrow::Cow; fn normalize(s: &str) -> Cow<'_, str> — вернёт заимствование, если менять нечего, и владеющую строку, если пришлось изменить. Это убирает лишние аллокации в горячем коде.

Функции и управляющие конструкции

Тело функции — это выражение; последнее выражение без ; становится возвращаемым значением. Именно поэтому a + b без точки с запятой возвращает результат, а a + b; — уже нет.

fn add(a: i32, b: i32) -> i32 {
    a + b                 // выражение без ; — возвращаемое значение
}

fn divmod(a: i32, b: i32) -> (i32, i32) { (a / b, a % b) }   // кортеж

fn greet(name: &str) -> String { format!("Привет, {name}!") }

fn main() {
    let (q, r) = divmod(17, 5);
    let msg = if q > 3 { "много" } else { "мало" };  // if — тоже выражение
    println!("{q} {r} {msg} {}", greet("Аня"));
}
let code = 404;
let text = match code {
    200 => "OK",
    301 | 302 => "Redirect",
    400..=499 => "Client error",
    other if other >= 500 => "Server error",  // guard: доп. условие на захваченное значение
    _ => "Unknown",                            // match должен быть исчерпывающим
};

// match по ссылке и по структуре
let pair = (0, -2);
match pair {
    (0, y) => println!("ось X, y = {y}"),
    (x, 0) => println!("ось Y, x = {x}"),
    (x, y) => println!("общая точка {x},{y}"),
}
// if let — когда интересен только один вариант
let config: Option<u32> = Some(8080);
if let Some(port) = config {
    println!("порт {port}");
}

// while let — цикл, пока шаблон совпадает
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{top}");
}

// Циклы
for i in 0..5 {}                       // 0..4, верхняя граница не включается
for i in (0..=10).step_by(2) {}        // 0,2,4,6,8,10
for (i, item) in ["a", "b"].iter().enumerate() { println!("{i}: {item}"); }

let result = loop {                    // loop умеет возвращать значение через break
    let n = 7;
    if n > 5 { break n * 2; }
};
println!("{result}");                  // 14

'outer: for x in 0..3 {                // метки позволяют выйти из внешнего цикла
    for y in 0..3 {
        if y == 1 { continue 'outer; }
        if x == 2 { break 'outer; }
    }
}

Владение, заимствование и времена жизни

Это ядро Rust и причина, по которой код либо компилируется, либо сразу безопасен. Правила: у значения один владелец; передача владельца — перемещение; можно временно одолжить значение по ссылке; при выходе владельца из области видимости вызывается Drop.

let s1 = String::from("hello");
let s2 = s1;                    // move: владение перешло к s2
// println!("{s1}");            // ошибка: borrow of moved value: `s1`

fn takes_ownership(s: String) { println!("{s}"); }
fn borrows(s: &String) -> usize { s.len() }
fn gives() -> String { String::from("new") }

let s3 = gives();
borrows(&s3);                   // заимствование, s3 остаётся владельцем
takes_ownership(s3);            // перемещение, после этого s3 недоступен
// Copy-типы копируются, а не перемещаются: числа, bool, char, кортежи из Copy-типов
let a = 5;
let b = a;
println!("{a} {b}");            // обе переменные живы

// Правила заимствования: либо много общих ссылок, либо одна изменяющая — но не оба сразу
let mut v = vec![1, 2, 3];
let first = &v[0];              // общая ссылка
// v.push(4);                   // ошибка: cannot borrow `v` as mutable, пока жива `first`
println!("{first}");
v.push(4);                      // теперь можно — NLL видит, что first больше не используется
// Времена жизни: функция возвращает ссылку, поэтому нужно связать вход и выход
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Структура не может хранить ссылку без аннотации времени жизни
struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn announce(&self, news: &str) -> &str {   // elision: результат связан с &self
        println!("Внимание: {news}");
        self.part
    }
}

// Elision: если у функции один вход-ссылка, время жизни выхода = её время жизни
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

Правила elision (когда аннотации можно опустить): у каждого входного параметра-ссылки своё время жизни; если вход ровно один — выход получает его; если есть &self или &mut self — выход получает время жизни self. Во всех остальных случаях компилятор потребует явный 'a. 'static означает «живёт всю программу» и подходит для строковых литералов, но не для данных, зависящих от рантайма.

Структуры, enum и impl

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    name: String,
    age: u8,
    active: bool,
}

impl User {
    // Ассоциированная функция-конструктор, без self
    fn new(name: impl Into<String>) -> Self {
        Self { name: name.into(), age: 0, active: true }
    }

    fn birthday(&mut self) {                 // &mut self — меняет состояние
        self.age = self.age.saturating_add(1);
    }

    fn is_adult(&self) -> bool { self.age >= 18 }   // &self — только чтение
}

let mut u = User::new("Анна");
u.birthday();
let copy = u.clone();
assert_eq!(u, copy);
println!("{u:?}");                            // Debug; {u:#?} — многострочно
// enum с данными в вариантах — замена иерархиям классов
#[derive(Debug)]
enum Shape {
    Circle { radius: f64 },
    Rect(f64, f64),
    Point,
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rect(w, h) => w * h,
            Shape::Point => 0.0,
        }
    }
}

// Изменение через match по &mut
fn scale(shape: &mut Shape, k: f64) {
    match shape {
        Shape::Circle { radius } => *radius *= k,   // *radius — разыменование ссылки из паттерна
        Shape::Rect(w, h) => { *w *= k; *h *= k; }
        Shape::Point => {}
    }
}

let shapes = vec![Shape::Rect(3.0, 4.0), Shape::Circle { radius: 1.0 }];
let total: f64 = shapes.iter().map(Shape::area).sum();
println!("{total}");
// Default, конверсии и оператор сложения через трейт из std::ops
#[derive(Debug, Default, PartialEq)]
struct Vector2 { x: f64, y: f64 }

impl std::ops::Add for Vector2 {
    type Output = Vector2;
    fn add(self, rhs: Self) -> Self::Output {
        Vector2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

let a = Vector2 { x: 1.0, y: 2.0 };
let b = Vector2 { x: 3.0, ..Default::default() };   // struct update syntax
let sum = a + b;
assert_eq!(sum, Vector2 { x: 4.0, y: 2.0 });

Метод into() работает бесплатно, если реализован From: impl From<Point> for Coord. Правило из стандартной библиотеки: реализуй From, получай Into автоматически, а TryFrom — для конверсий, которые могут не удаться.

Трейты, generics и trait objects

trait Summary {
    fn summarize(&self) -> String;           // обязательный метод

    fn preview(&self) -> String {            // метод по умолчанию
        format!("{}…", self.summarize())
    }
}

struct Post { title: String }
struct Comment { text: String }

impl Summary for Post {
    fn summarize(&self) -> String { self.title.clone() }
}
impl Summary for Comment {
    fn summarize(&self) -> String { format!("{}…", &self.text) }
}

// Статическая диспетчеризация: компилятор мономорфизирует под каждый тип
fn notify<T: Summary>(item: &T) { println!("{}", item.summarize()); }

// Динамическая: один код, вызов через vtable
fn notify_dyn(item: &dyn Summary) { println!("{}", item.summarize()); }

let posts: Vec<Box<dyn Summary>> = vec![
    Box::new(Post { title: "Rust".into() }),
    Box::new(Comment { text: "полезно".into() }),
];
for p in &posts { notify_dyn(p.as_ref()); }
// От нескольких трейтов и через where — читается лучше на длинных сигнатурах
fn describe<T>(v: &T) -> String
where
    T: std::fmt::Display + Clone,
{
    format!("{}", v.clone())
}

// Generic-структура с ограничением
struct Pair<T: std::fmt::Display> { a: T, b: T }

impl<T: std::fmt::Display> Pair<T> {
    fn show(&self) -> String { format!("({}, {})", self.a, self.b) }
}

// impl Trait в возвращаемом типе: «какой-то итератор», конкретный тип скрыт
fn evens(v: &[i32]) -> impl Iterator<Item = i32> + '_ {
    v.iter().copied().filter(|n| n % 2 == 0)
}

// impl Trait в аргументе — синтаксический сахар над generic
fn log(msg: impl std::fmt::Display) { println!("[log] {msg}"); }
use std::fmt;

struct Point { x: i32, y: i32 }

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// Blanket-реализация: реализуй Display — получишь ToString бесплатно
let p = Point { x: 1, y: 2 };
assert_eq!(p.to_string(), "(1, 2)");

Когда что выбирать: generic (статическая диспетчеризация) быстрее и позволяет инлайнинг, но раздувает бинарник под каждый тип; dyn Trait даёт один код и heterogenous-коллекции, но добавляет разыменование указателя. Для коллекций разнородных объектов — только Box<dyn Trait>.

Коллекции: Vec, HashMap, HashSet, BTreeMap

// Vec — динамический массив
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.extend([2, 3, 4]);
v.insert(0, 0);                 // вставка по индексу — O(n)
let last = v.pop();             // Option<i32>
let v2 = vec![0; 5];            // [0, 0, 0, 0, 0]
let slice: &[i32] = &v[1..3];
v.retain(|&x| x != 2);          // удалить все двойки
v.sort_by_key(|&x| std::cmp::Reverse(x));  // сортировка по убыванию

let sum: i32 = v.iter().sum();
let idx = v.binary_search(&3);  // Ok(pos) или Err(точка вставки) — только на отсортированном
use std::collections::HashMap;

let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert("Аня".to_string(), 10);
scores.entry("Боря".to_string()).or_insert(0);          // вставить, если нет
*scores.entry("Аня".to_string()).or_insert(0) += 5;      // классический подсчёт
scores.remove("Боря");

match scores.get("Аня") {            // get возвращает Option<&u32>
    Some(score) => println!("Аня: {score}"),
    None => println!("нет записи"),
}

for (name, score) in &scores {       // порядок обхода не гарантирован
    println!("{name}: {score}");
}

// Собрать HashMap из итератора за один проход
let map: HashMap<&str, usize> = ["a", "b", "a"].into_iter()
    .fold(HashMap::new(), |mut acc, k| { *acc.entry(k).or_insert(0) += 1; acc });
assert_eq!(map["a"], 2);
use std::collections::{BTreeMap, HashSet};

// HashSet — уникальные значения, среднее O(1)
let mut seen = HashSet::new();
assert!(seen.insert("a"));      // true — новый элемент
assert!(!seen.insert("a"));     // false — уже был

let nums: HashSet<i32> = HashSet::from([1, 2, 2, 3]);
assert_eq!(nums.len(), 3);      // дубликаты схлопнулись
assert!(nums.contains(&2));     // поиск за O(1)
let diff: Vec<&i32> = nums.difference(&HashSet::from([2])).collect();   // [1, 3]

// BTreeMap — ключи всегда отсортированы, O(log n), есть range
let mut ordered: BTreeMap<u32, &str> = BTreeMap::new();
ordered.insert(3, "три");
ordered.insert(1, "один");
ordered.insert(2, "два");
for (k, v) in ordered.range(2..) { println!("{k} = {v}"); }   // 2, 3

Выбор контейнера: Vec — по умолчанию для любых последовательностей; HashMap/HashSet — когда нужен быстрый поиск и порядок неважен; BTreeMap/BTreeSet — когда нужен порядок ключей или диапазонные запросы; VecDeque — очередь с быстрым добавлением/удалением с обоих концов.

Option, Result и оператор ?

В Rust нет null и исключений: отсутствие значения выражает Option<T>, ошибку — Result<T, E>. Компилятор заставляет обработать оба случая — именно поэтому забытые проверки на ошибки не доживают до продакшена.

let maybe: Option<i32> = Some(42);
let none: Option<i32> = None;

let value = maybe.unwrap_or(0);                 // 42
let doubled = maybe.map(|n| n * 2);             // Some(84)
let filtered = maybe.filter(|&n| n > 40);       // Some(42)
let flat = maybe.and_then(|n| if n > 0 { Some(n as f64) } else { None });

if let Some(n) = filtered { println!("{n}"); }
println!("{}", none.unwrap_or_default());       // 0

// unwrap/expect паникуют — это нормально в прототипе, но не в библиотеке
let must = maybe.expect("порт обязателен");
use std::num::ParseIntError;

fn parse_port(s: &str) -> Result<u16, ParseIntError> {
    let port: u16 = s.parse()?;      // `?` вернёт Err, если парсинг не удался
    Ok(port)                        // Ok(port) == return Ok(port)
}

fn half(n: i32) -> Result<i32, String> {
    if n % 2 == 0 { Ok(n / 2) } else { Err(format!("{n} нечётное")) }
}

fn chain() -> Result<i32, String> {
    let a = half(10)?;                        // 5
    let b = half(a).map_err(|e| format!("сбой: {e}"))?;  // Err → другая ошибка
    Ok(a + b)
}

// main тоже может возвращать Result — тогда ? работает на верхнем уровне
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port = parse_port("8080")?;
    println!("порт {port}");
    Ok(())
}
// Комбинаторы, которые убирают match целиком
fn to_u32(s: &str) -> u32 {
    s.trim()
        .parse::<u32>()
        .ok()                          // Result → Option
        .filter(|n| *n > 0)
        .unwrap_or(1)                  // дефолт вместо паники
}

let raw = ["10", "abc", "0"];
let parsed: Vec<u32> = raw.iter().map(|s| to_u32(s)).collect();
assert_eq!(parsed, [10, 1, 1]);

// ok_or превращает Option в Result с нужной ошибкой
let missing: Option<&str> = None;
let res = missing.ok_or("значение отсутствует");
assert!(res.is_err());

Шпаргалка по методам: map — преобразовать успех; and_then — продолжить цепочку; unwrap_or / unwrap_or_else / unwrap_or_default — достать значение с дефолтом; ok_or / ok_or_else — Option → Result; ok — Result → Option; is_some/is_none/is_ok/is_err — проверки без разворачивания; expect — как unwrap, но с сообщением. Паника (panic!, unwrap, индекс за границей) — для багов и нарушенных инвариантов, а не для ожидаемых ошибок.

Умные указатели: Box, Rc, Arc, RefCell, Mutex

// Box<T> — владеющее значение в куче. Нужен для рекурсивных типов и больших данных
#[derive(Debug)]
struct ListNode {
    value: i32,
    next: Option<Box<ListNode>>,   // без Box тип был бы бесконечного размера
}

let list = ListNode {
    value: 1,
    next: Some(Box::new(ListNode { value: 2, next: None })),
};
println!("{list:?}");

let boxed = Box::new(5);
let inner: i32 = *boxed;           // разыменование через Deref
use std::cell::RefCell;
use std::rc::Rc;

// Rc<T> — общее владение в одном потоке (счётчик ссылок). Только для чтения!
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));   // RefCell добавляет внутреннюю изменяемость
let a = Rc::clone(&shared);         // +1 к счётчику, данные не копируются
a.borrow_mut().push(4);             // проверка правил заимствования в рантайме
assert_eq!(shared.borrow().len(), 4);
assert_eq!(Rc::strong_count(&shared), 2);

drop(a);                            // -1
assert_eq!(Rc::strong_count(&shared), 1);

Rc не потокобезопасен (не реализует Send). Для многопоточности берут Arc — тот же счётчик ссылок на атомарных операциях. RefCell паникует, если нарушить правила заимствования в рантайме: два borrow_mut() одновременно — паника. Поэтому Rc<RefCell<T>> — это компромисс «гибко, но проверка в рантайме».

use std::sync::{Arc, Mutex};

// Arc<Mutex<T>> — разделяемое изменяемое состояние между потоками
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..4 {
    let c = Arc::clone(&counter);
    handles.push(std::thread::spawn(move || {
        for _ in 0..1000 {
            *c.lock().unwrap() += 1;    // lock() возвращает Result — мьютекс может быть отравлен
        }
    }));
}
for h in handles { h.join().unwrap(); }
assert_eq!(*counter.lock().unwrap(), 4000);

Сводка: Box — владение в куче, один владелец; Rc — несколько владельцев в одном потоке; Arc — несколько владельцев между потоками; RefCell — изменяемость через общую ссылку с рантайм-проверкой; Mutex/RwLock — синхронизация между потоками; Cell — для Copy-типов без borrow-проверок. Приём «отравившегося» мьютекса (PoisonError) означает, что поток паниковал, держа блокировку: данные могут быть в неконсистентном состоянии.

Итераторы и замыкания

Итераторы в Rust ленивы: цепочка map/filter не выполняется, пока её не «потребит» collect, sum, for или другой consumer. Компилятор инлайнит такие цепочки в цикл без промежуточных аллокаций.

let v = vec![1, 2, 3, 4, 5, 6];

let squares: Vec<i32> = v.iter()
    .filter(|&&x| x % 2 == 0)   // получаем &&i32, паттерн разворачивает
    .map(|&x| x * x)            // &i32 → i32
    .collect();

let sum = v.iter().fold(0, |acc, x| acc + x);        // 21
let total: i32 = v.iter().sum();
let max = v.iter().max();                            // Option<&i32>
let joined = v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join("+");

let pairs: Vec<(i32, i32)> = vec![1, 2, 3].into_iter().zip(vec![10, 20, 30]).collect();
let chunks: Vec<usize> = v.chunks(2).map(|c| c.len()).collect();
let windows: Vec<&[i32]> = v.windows(3).collect();
let taken: Vec<i32> = v.iter().copied().skip(1).take(3).collect();
// iter() → &T, iter_mut() → &mut T, into_iter() → T (потребляет коллекцию)
let mut words = vec!["b", "a"];
for w in words.iter_mut() { *w = "x"; }
let owned: Vec<String> = words.iter().map(|s| s.to_uppercase()).collect();

// Замыкания: || вместо (), типы выводятся из первого использования
let add = |a, b| a + b;
let n = 10;
let capture_by_ref = |x: i32| x + n;          // захватывает n по ссылке
let capture_by_move = move |x: i32| x + n;    // переносит n внутрь
drop(capture_by_move);
println!("{n}");                              // n всё ещё доступен: i32 — Copy

// Ленивое вычисление: ничего не происходит, пока нет consumer
let lazy = v.iter().map(|x| { println!("считаю {x}"); x * 2 });
let collected: Vec<i32> = lazy.collect();      // вот здесь побегут print!
// Три трейта замыканий — по способу захвата
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }          // только читает
fn call_twice<F: FnMut(i32)>(mut f: F) { f(1); f(2); }              // меняет захваченное
fn build<F: FnOnce() -> String>(f: F) -> String { f() }             // потребляет (move)

// Возврат собственного итератора с захватом
fn evens(v: &[i32]) -> impl Iterator<Item = i32> + '_ {
    v.iter().copied().filter(|n| n % 2 == 0)
}

// Частотный словарь одной цепочкой
use std::collections::HashMap;
let freq: HashMap<char, usize> = "abracadabra".chars()
    .fold(HashMap::new(), |mut m, ch| { *m.entry(ch).or_insert(0) += 1; m });
assert_eq!(freq[&'a'], 5);

Модули, видимость и пакеты

// src/lib.rs — корень крейта
mod math;                 // подключает src/math.rs
pub mod api;              // публичный модуль, виден снаружи

use crate::math::add;     // абсолютный путь от корня крейта
use std::collections::HashMap;

pub fn run() -> i32 { add(1, 2) }

// src/math.rs
// pub fn add(a: i32, b: i32) -> i32 { a + b }
// fn helper() {}                 // приватная — видна только внутри math
// pub(crate) fn internal() {}    // видна всему крейту, но не наружу
// Вложенные модули и путь через super
mod outer {
    pub mod inner {
        pub fn hello() -> &'static str { "hello" }

        fn private() {}

        pub(super) fn for_parent() {}    // видно только родителю outer

        pub fn via_parent() -> &'static str {
            super::outer_path()          // super внутри inner — это модуль outer
        }
    }

    pub fn call() -> &'static str {
        inner::hello()                   // путь внутри своего модуля
    }

    fn outer_path() -> &'static str { "outer" }
}

// Реэкспорт: пользователь крейта видит короткий путь
pub use outer::inner::hello;
# Пакет = один Cargo.toml, крейт = единица компиляции.
# Один пакет может содержать несколько крейтов: библиотеку и бинарники.
[package]
name = "myapp"
version = "0.1.0"
edition = "2024"

[lib]
name = "myapp"

[[bin]]
name = "myapp-cli"
path = "src/bin/cli.rs"

Правила видимости: элементы приватны по умолчанию; pub открывает наружу, pub(crate) — внутри крейта, pub(super) — родительскому модулю, pub(in path) — конкретному пути. Поле структуры приватно вместе со структурой, поэтому конструкторы (fn new) обычно и есть та самая публичная точка входа, которая сохраняет инварианты.

Обработка ошибок: thiserror и anyhow

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("файл не найден: {0}")]
    NotFound(String),

    #[error("ошибка ввода-вывода")]
    Io(#[from] std::io::Error),          // #[from] даёт конверсию через `?`

    #[error("некорректный порт {port}: {source}")]
    BadPort { port: String, source: std::num::ParseIntError },
}

fn read_config(path: &str) -> Result<String, AppError> {
    let text = std::fs::read_to_string(path)?;   // io::Error → AppError::Io автоматически
    if text.is_empty() {
        return Err(AppError::NotFound(path.to_string()));
    }
    Ok(text)
}
use anyhow::{Context, Result};

// anyhow — для приложений: одна стирающая тип ошибка с контекстом
fn load(path: &str) -> Result<Config> {
    let text = std::fs::read_to_string(path)
        .with_context(|| format!("не удалось прочитать {path}"))?;
    let cfg: Config = serde_json::from_str(&text)
        .context("некорректный JSON в конфиге")?;
    Ok(cfg)
}

#[derive(serde::Deserialize)]
struct Config { host: String, port: u16 }
// Своя ошибка без зависимостей
#[derive(Debug)]
enum MyError { Empty, TooLong(usize) }

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::Empty => write!(f, "пустое значение"),
            MyError::TooLong(n) => write!(f, "слишком длинное: {n}"),
        }
    }
}

impl std::error::Error for MyError {}   // теперь её можно класть в Box<dyn Error> и `?`

Правило выбора: библиотеки возвращают конкретный enum-ошибку (thiserror) — пользователю важно матчить варианты; приложения используют anyhow::Result — важен только контекст и цепочка причин. Не делай Box<dyn Error> в публичном API библиотеки: вызывающий не сможет отличить один сбой от другого.

Многопоточность: потоки, каналы, Send/Sync

use std::thread;

let handle = thread::spawn(|| {
    for i in 1..=3 { println!("поток: {i}"); }
});
handle.join().unwrap();      // ждём завершения; Err, если поток паникнул
println!("главный поток");
// Scoped threads (Rust 1.63+): потоки гарантированно завершаются до выхода из scope,
// поэтому можно заимствовать локальные данные без Arc
let data = vec![1, 2, 3];
thread::scope(|s| {
    s.spawn(|| println!("сумма: {}", data.iter().sum::<i32>()));
    s.spawn(|| println!("длина: {}", data.len()));
});
println!("{data:?}");        // data по-прежнему доступна
use std::sync::mpsc;

// Канал: несколько отправителей, один получатель
let (tx, rx) = mpsc::channel();
for id in 0..3 {
    let tx = tx.clone();
    thread::spawn(move || tx.send(id * 10).unwrap());
}
drop(tx);                    // закрываем исходный отправитель, иначе цикл повиснет

for received in rx {         // итерация завершится, когда все tx будут дропнуты
    println!("получено: {received}");
}
// Send — тип можно передать в другой поток. Sync — на него можно ссылаться из потоков.
// Rc/RefCell не Send и не Sync — компилятор не даст их протащить в thread::spawn
use std::rc::Rc;
let bad = Rc::new(1);
// thread::spawn(move || println!("{bad}"));  // ошибка: `Rc<i32>` cannot be sent between threads safely

// Проверка статически: функция требует Send
fn assert_send<T: Send>(_: &T) {}
assert_send(&std::sync::Mutex::new(0));   // Mutex<i32>: Send + Sync
use std::sync::RwLock;

// RwLock: много читателей ИЛИ один писатель — быстрее Mutex на read-heavy нагрузке
let cache = std::sync::Arc::new(RwLock::new(std::collections::HashMap::<String, u32>::new()));
{
    let mut w = cache.write().unwrap();
    w.insert("версия".into(), 2);
}
{
    let r = cache.read().unwrap();
    println!("{:?}", r.get("версия"));
}

Практика: Mutex для коротких критических секций и «редко читаем, часто пишем»; RwLock — когда читателей заметно больше; атомики (AtomicUsize и др.) — для счётчиков и флагов без блокировок; каналы — для передачи владения между потоками вместо общей памяти. Гонки данных в безопасном Rust невозможны по построению: компилятор требует Send/Sync и не даёт двум потокам изменяющие ссылки на одни данные.

async/await и tokio

async fn не запускает код: он возвращает future, которую должен опросить executor. Стандартная библиотека даёт синтаксис, но не рантайм — поэтому на практике подключают tokio.

[dependencies]
tokio = { version = "1", features = ["full"] }
use std::time::Duration;
use tokio::time::sleep;

#[tokio::main]                    // разворачивается в блокирующий запуск рантайма
async fn main() {
    let a = fetch(1);
    let b = fetch(2);
    let (ra, rb) = tokio::join!(a, b);   // конкурентно, но в одном потоке
    println!("{ra} {rb}");
}

async fn fetch(id: u32) -> u32 {
    sleep(Duration::from_millis(100)).await;   // .await отдаёт управление, не блокирует поток
    id * 10
}
// Запуск фоновой задачи: требует 'static и Send
let handle = tokio::spawn(async {
    sleep(Duration::from_millis(50)).await;
    42
});
let value = handle.await.unwrap();
assert_eq!(value, 42);

// Таймаут и select! — «что случится раньше»
use tokio::sync::mpsc;
let (_tx, mut rx) = mpsc::channel::<u32>(8);

tokio::select! {
    Some(v) = rx.recv() => println!("получено {v}"),
    _ = sleep(Duration::from_secs(1)) => println!("таймаут"),
}
// Асинхронный трейт: с Rust 1.75 `async fn` можно объявлять прямо в трейте
trait Fetcher {
    async fn fetch(&self, id: u32) -> String;
}

// Для dyn-совместимости и старых версий используют крейт async-trait
// #[async_trait::async_trait]
// trait Fetch { async fn get(&self, url: &str) -> anyhow::Result<String>; }

Ключевые отличия от потоков: future ленива (ничего не выполняется без .await или spawn); .await уступает управление, а не блокирует ОС-поток; tokio::spawn требует Send + 'static; для CPU-bound задач async не помогает — нужен spawn_blocking или пул потоков (rayon). Библиотеки для сети: reqwest (HTTP), axum/actix-web (серверы), sqlx/diesel (БД).

Тесты: #[test] и cargo test

fn add(a: i32, b: i32) -> i32 { a + b }
fn divide(a: i32, b: i32) -> i32 {
    assert!(b != 0, "делить на ноль");
    a / b
}

#[cfg(test)]                 // модуль компилируется только в тестовой сборке
mod tests {
    use super::*;            // импорт всего из родительского модуля

    #[test]
    fn add_works() {
        assert_eq!(add(2, 2), 4);
        assert_ne!(add(1, 1), 3);
    }

    #[test]
    #[should_panic(expected = "делить на ноль")]
    fn divide_by_zero_panics() {
        divide(1, 0);
    }

    #[test]
    fn parse_invalid_is_err() -> Result<(), Box<dyn std::error::Error>> {
        assert!("abc".parse::<i32>().is_err());
        Ok(())               // тест может возвращать Result и использовать `?`
    }
}
cargo test                       # unit + integration + doctests
cargo test add_works             # фильтр по имени
cargo test -- --nocapture        # показать println!
cargo test -- --test-threads=1   # последовательно (для общих ресурсов)
cargo test --release             # тесты оптимизированной сборки
cargo test --doc                 # только примеры из доккомментариев
// Доктест: код из /// компилируется и запускается как тест
/// Складывает два числа.
///
/// ```
/// assert_eq!(mycrate::add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

// Асинхронный тест
#[tokio::test]
async fn async_works() {
    assert_eq!(tokio::spawn(async { 1 + 1 }).await.unwrap(), 2);
}

Интеграционные тесты кладут в tests/ в корне крейта — они компилируются как отдельные крейты и используют только публичный API. Ресурсы для тестов (tempfile для файлов, mockall для моков) добавляй в [dev-dependencies]. Полезные атрибуты: #[ignore] — пропустить тяжёлый тест (cargo test -- --ignored), #[should_panic] — ожидание паники. Для покрытия — cargo-llvm-cov, для property-based тестов — proptest.

Макросы, derive и serde

// macro_rules! — декларативные макросы, раскрываются на этапе компиляции
macro_rules! vec_of_strings {
    ($($item:expr),* $(,)?) => {           // $(...)* — повторение, $(,)? — необязательная запятая
        vec![$(String::from($item)),*]
    };
}

macro_rules! check {
    ($cond:expr, $msg:literal) => {
        if !$cond { panic!("{}", $msg); }
    };
}

fn main() {
    let names = vec_of_strings!("Аня", "Боря");
    check!(names.len() == 2, "ожидалось два имени");
    println!("{names:?}");
}
// derive генерирует реализации трейтов автоматически
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
struct Config {
    host: String,
    port: u16,
}

let c = Config::default();
let c2 = c.clone();
println!("{c:?} {c2:?}");

// Copy — только для «простых» типов без Drop
#[derive(Debug, Clone, Copy, PartialEq)]
struct Meters(f64);

// Debug можно писать вручную, чтобы скрыть чувствительные поля
#[derive(Clone)]
struct Token(String);
impl std::fmt::Debug for Token {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Token(***)")
    }
}
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct User {
    id: u64,
    name: String,
    #[serde(default)]                 // отсутствующее поле → Default
    active: bool,
    #[serde(rename = "createdAt")]    // имя в JSON отличается от поля Rust
    created_at: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
}

fn roundtrip() -> Result<(), serde_json::Error> {
    let user = User {
        id: 1,
        name: "Аня".into(),
        active: true,
        created_at: "2026-09-19".into(),
        email: None,
    };
    let json = serde_json::to_string(&user)?;
    println!("{json}");
    let back: User = serde_json::from_str(&json)?;
    assert_eq!(user, back);
    Ok(())
}
// Работа с произвольным JSON без структуры
use serde_json::{json, Value};

let raw = r#"{"name": "Аня", "tags": ["rust", "async"]}"#;
let v: Value = serde_json::from_str(raw).unwrap();
println!("{}", v["name"]);                  // "Аня"
println!("{}", v["tags"][1]);               // "async"

let built = json!({ "ok": true, "count": 3 });
assert_eq!(built["count"], 3);

Правила: macro_rules! — для повторяющегося однотипного кода и DSL внутри проекта (гигиена имён, $crate для путей); процедурные макросы (serde, thiserror, clap) — это отдельные крейты с proc-macro = true, они парсят токены и генерируют код. Если можешь решить задачу generic-функцией или трейтом — используй их, а не макрос: макросы сложнее отлаживать (cargo expand в помощь).

Частые ошибки

Ошибка Причина Решение
borrow of moved value Значение перемещено в другую переменную или функцию, старая недоступна Передавай по ссылке &T, используй .clone() или верни владение из функции
cannot borrow X as mutable more than once Две изменяющие ссылки или изменяющая вместе с общей на одну область Разведи области видимости: закрой читателя println! или блоком {} до borrow_mut()
cannot borrow as mutable, as it is behind a & reference Метод меняет данные, а параметр пришёл по &T Поменяй сигнатуру на &mut self, а тип — на &mut T
missing lifetime specifier Компилятор не может связать время жизни входа и выхода Добавь <'a> и расставь &'a, либо прими владение (String/Vec) вместо ссылки
does not live long enough Ссылка переживает владельца — возврат ссылки на локальную переменную Верни владеющее значение, либо пусть владелец живёт в вызывающем коде
use of moved value: String в цикле String перемещён на первой итерации Заимствуй (&s), клонируй, либо храни значения в Vec и итерируйся по ссылкам
self takes ownership of Point, which is behind a & reference Метод потребляет self, а вызывается на ссылке Используй &self/&mut self или #[derive(Clone)] + .clone()
String vs &str: expected &str, found String Типы различаются, deref coercion не сработала (например, в generic) Передай &s или s.as_str(); в сигнатуре принимай &str
Rc<RefCell<T>> паникует already borrowed Два borrow_mut() одновременно или borrow() на время мутации Сузь область видимости до одного выражения, вынеси значение из RefCell
Rc/RefCell not Send: cannot be sent between threads safely Rc не потокобезопасен Замени на Arc, RefCell → Mutex/RwLock, либо используй thread::scope
Mutex PoisonError / lock().unwrap() паникует Поток паникнул, удерживая блокировку Обработай PoisonError через `unwrap_or_else(
unwrap() на None/Err Паника вместо обработки unwrap_or, unwrap_or_else, ?, expect с осмысленным сообщением только там, где сбой невозможен
index out of bounds Выход за границы Vec/массива/строки .get(i) возвращает Option, у срезов — .get(a..b); проверяй границы UTF-8 у строк
async fn/future не выполняется Future ленива, её никто не опросил Добавь .await, tokio::spawn или запусти через #[tokio::main]
future cannot be sent between threads safely в tokio::spawn Держишь Rc/RefCell/не-Send через .await Не пересекай .await с не-Send заимствованием: оберни в блок или замени тип
cannot find macro / cannot find crate Не подключена зависимость или фича cargo add crate --features ..., проверь [dependencies] и use
Overengineering с unsafe Ручное управление памятью там, где хватает безопасного Rust Сначала пробуй Rc/RefCell/Arc/индексы; unsafe — только с // SAFETY:-комментарием и обоснованием
Целочисленное переполнение в release Debug паникует, release «заворачивает» checked_*, saturating_*, wrapping_* или #[deny(arithmetic_overflow)]

Ресурсы

Что дальше


Связанные шпаргалки: C++, C++ Core Guidelines, Python, Go, Си