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)] |
Ресурсы
- The Rust Programming Language — официальная «книга», главный учебник с нуля
- Rust by Example — примеры на каждую конструкцию
- Стандартная библиотека —
Option,Result,Vec, трейты - docs.rs — документация всех крейтов экосистемы, собранная из исходников
- Rust Reference — формальная спецификация языка
- Rustonomicon —
unsafe, внутренности и UB - Cargo Book — манифест, фичи, workspace
- Clippy — линтер и каталог проверок
- Rustfmt — единый стиль кода
- Rust Playground — запуск кода в браузере без установки
- Rust Design Patterns — идиомы и паттерны проектирования
Что дальше
- Шпаргалка по Python: синтаксис, коллекции, ООП, async — сравнение подходов и типизации
- Шпаргалка по uv: пакетный менеджер Python на Rust — как Rust ускоряет инструменты Python
- Шпаргалка по Linux: файлы, права, процессы, сеть — командная строка рядом с системным кодом
- Шпаргалка по Go: горутины, каналы, интерфейсы — другой подход к конкурентности и простоте
- Шпаргалка по Си: указатели, память, UB — то, что Rust делает безопасным на этапе компиляции
Связанные шпаргалки: C++, C++ Core Guidelines, Python, Go, Си