Skip to content

Rust Шпаргалка

Системный язык, сфокусированный на безопасности, скорости и конкурентности.

01

Основы

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

Переменные Rust неизменяемы по умолчанию — это ключевая функция безопасности, предотвращающая случайные мутации. Используйте 'mut' только когда действительно нужно изменить значение. 'const' требует явного типа и встраивается при компиляции, а 'static' имеет фиксированный адрес в памяти со временем жизни 'static.

rust
let x = 5;            // immutable by default
let mut y = 10;        // mutable with 'mut'
y += 1;                // OK, y is mutable

const MAX: u32 = 100;  // compile-time constant
static GREETING: &str = "Hi"; // global static

let z: i32 = -5;       // explicit type annotation

Shadowing

Shadowing позволяет переиспользовать имя переменной, изменяя её тип или значение. В отличие от 'mut', shadowing создаёт новую привязку — полезно для преобразования данных (например, парсинг строки в int) без придумывания нового имени. Старое значение уничтожается после новой привязки.

rust
let x = 5;
let x = x * 2;         // shadow previous x, now 10
let x = "text";        // can even change type!
println!("{}", x);     // "text"

// shadowing vs mut: shadowing creates a NEW variable
// with the same name, allowing type changes

Комментарии и печать

Используйте /// для документации элементов (показывается 'cargo doc') и //! для документации модулей/крейтов. println! пишет в stdout, eprintln! в stderr. Плейсхолдер {} поддерживает спецификации форматирования: > для правого выравнивания, < для левого, ^ для центрирования, .N для точности, #b/#o/#x для binary/octal/hex.

rust
// Line comment
/* Block comment */
/// Doc comment (renders in rustdoc)
//! Module-level doc comment

let name = "Alice";
println!("Hello, {}!", name);        // format string
println!("{0} {1} {0}", "a", "b");   // positional args
println!("{:>5}", 42);               // right-align, width 5
println!("{:.2}", 3.14159);          // 2 decimal places: 3.14
println!("{:#b}", 0b1010);           // binary: 0b1010
eprintln!("Error to stderr");        // error output

Обзор типов данных

В Rust нет неявного преобразования типов — используйте 'as' для приведения. i32 — целочисленный тип по умолчанию, f64 — float по умолчанию. usize используется для индексации и размеров. char — полное Unicode scalar value (не байт), поэтому '🦀' — один char.

rust
// Scalar types
let a: i32 = 42;          // signed 32-bit integer
let b: u64 = 100;         // unsigned 64-bit
let c: f64 = 3.14;        // 64-bit float (default)
let d: bool = true;
let e: char = '🦀';        // 4-byte Unicode scalar

// isize/usize: pointer-sized (32 or 64 bit depending on arch)
let len: usize = vec![1,2,3].len();

// Compound types
let pair: (i32, &str) = (1, "hello");
let arr: [i32; 3] = [0; 3];   // [0, 0, 0]

Приведение типов и алиасы

Приведение 'as' не проверяется и может терять данные (например, 300u16 as u8 обёртывается в 44). Для безопасных преобразований используйте типажи From/Into, реализованные для преобразований без потерь. Алиасы типов улучшают читаемость без создания новых типов — используйте 'struct NewType(i32)' для паттерна distinct newtype.

rust
// 'as' keyword for primitive casts (may truncate)
let x: i32 = 42;
let y: f64 = x as f64;        // 42.0
let z: u8 = 300u16 as u8;     // truncated: 44

// From/Into traits for safe conversion
let s = String::from("hi");
let owned: String = "hi".into();

// Type aliases
type Kilometers = i32;
let distance: Kilometers = 5;
02

Строки

String vs &str

Различие между String (владелец, куча) и &str (заимствованный срез) фундаментально в Rust. Используйте String, когда нужно владеть/изменять/наращивать текст; &str — когда нужно только читать. &str может указывать на буфер String или строковый литерал в бинарнике. Предпочитайте &str в параметрах функций для гибкости.

rust
// &str: immutable string slice (borrowed)
let s1: &str = "literal";           // stored in binary
let s2: &str = &s3[..2];            // slice of a String

// String: owned, growable, heap-allocated
let mut s3: String = String::from("hello");
s3.push_str(", world");             // append
s3.push('!');                       // append char
s3.insert(0, '>');                  // insert at index
println!("{}", s3);                 // >hello, world!

// Convert: &str -> String
let owned = "literal".to_string();
let owned2 = String::from("literal");

Методы String

Методы String возвращают новые owned Strings вместо модификации на месте (кроме методов push_*/insert у изменяемых Strings). split() возвращает итератор, поэтому используйте .collect() для материализации. Обратите внимание, что индексация s[0] НЕ разрешена для строк, так как границы UTF-8 не совпадают с байтовыми индексами — используйте s.chars().nth(0).

rust
let s = String::from("Hello, World");

// inspection
println!("len: {}", s.len());              // 12
println!("is_empty: {}", s.is_empty());    // false
println!("contains 'World': {}", s.contains("World")); // true
println!("starts_with 'Hello': {}", s.starts_with("Hello")); // true

// transformation
let upper = s.to_uppercase();              // HELLO, WORLD
let replaced = s.replace("o", "0");        // Hell0, W0rld
let trimmed = "  hi  ".trim().to_string(); // hi

// splitting
for word in s.split(", ") {
    println!("{}", word); // Hello / World
}
let parts: Vec<&str> = s.split_whitespace().collect();

Форматирование и конкатенация

Оператор + забирает владение левой String и заимствует правую (&str). Поэтому s1 становится недействительным после s1 + &s2. Для цепочки нескольких строк предпочитайте format!, который более читаем и не перемещает операнды. concat! работает только с литералами и производит &'static str.

rust
// format! macro creates a new String
let name = "Alice";
let msg = format!("Hi {}, you have {} messages", name, 5);

// concatenation
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2;        // s1 moved, s2 borrowed
// s1 is now invalid!

let s4 = format!("{}{}", s2, s3); // neither moved

// concat! macro for literals
let s5 = concat!("foo", "bar"); // "foobar" at compile time

Итерация символов и байтов

Строки Rust закодированы в UTF-8, поэтому байтовый индекс != индексу символа. chars() итерирует Unicode scalar values (O(n) для декодирования), bytes() — сырые байты. Срез [n..m] паникует, если n или m попадают в середину многобайтового символа. Для байтового доступа преобразуйте в Vec<u8> через as_bytes().

rust
let s = "héllo";  // é is 2 bytes in UTF-8

// iterate over chars (Unicode scalar values)
for c in s.chars() {
    print!("{} ", c);  // h é l l o
}

// iterate over bytes
for b in s.bytes() {
    print!("{} ", b);  // 104 195 169 108 108 111
}

// get char at index (O(n) — must walk UTF-8)
let third = s.chars().nth(2); // Some('l')

// string slices must be on char boundaries
let slice = &s[0..1]; // "h" — OK
// let bad = &s[0..2]; // PANIC if mid-é!

Парсинг и преобразование

parse() возвращает Result, так как строка может не быть корректным числом — всегда обрабатывайте ошибку. Синтаксис turbofish parse::<T>() позволяет указать тип инлайн. Преобразование String в &str бесплатное (просто заимствование), но &str в String выделяет память. collect() может собрать String из итератора символов.

rust
// String/str -> number
let n: i32 = "42".parse().unwrap();
let n2 = "42".parse::<i32>().unwrap(); // turbofish syntax
let f: f64 = "3.14".parse().unwrap();

// number -> String
let s = 42.to_string();
let s2 = format!("{}", 42);

// String -> &str (free, just dereference)
let owned = String::from("hi");
let borrowed: &str = &owned;

// collect chars into String
let upper: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
03

Структуры данных

Массивы и срезы

Массивы [T; N] имеют фиксированный размер, известный при компиляции, и живут в стеке. Срезы &[T] — толстые указатели (указатель + длина), заимствующие непрерывную последовательность — они позволяют функциям принимать любой массив или вектор без учёта размера. Используйте срезы в сигнатурах функций для общности.

rust
// Fixed-size array (stack allocated)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5];           // [0, 0, 0, 0, 0]
println!("first: {}", arr[0]);
println!("len: {}", arr.len());

// Slice: a view into an array/vector
let slice: &[i32] = &arr[1..3];  // [2, 3]
let full: &[i32] = &arr;          // whole array
let first = &arr[..1];            // [1]

// iterating
for n in &arr {
    println!("{}", n);
}

Векторы (Vec<T>)

Vec<T> — растущий массив Rust, поддерживаемый памятью кучи с удвоением ёмкости. push/pop — O(1) амортизированно; insert/remove — O(n) из-за сдвига элементов. Используйте .get(i) вместо v[i] для безопасного доступа (возвращает Option). into_iter() потребляет вектор, возвращая owned значения.

rust
// growable heap array
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3];          // macro shorthand

v.push(4);                        // append
v.pop();                          // remove last -> Option
v.insert(0, 0);                   // insert at index (O(n))
v.remove(0);                      // remove at index (O(n))
v.extend([5, 6]);                 // append multiple

// access
println!("{}", v[0]);             // panics if out of bounds
println!("{:?}", v.get(0));       // Some(&4) — safe

// iterate by value, ref, or mut ref
for n in &v { print!("{}", n); }
for n in &mut v { *n *= 2; }      // double each
let owned: Vec<i32> = v.into_iter().collect();

HashMap и BTreeMap

HashMap использует хэширование для O(1) среднего доступа, но не имеет упорядочивания. BTreeMap использует B-дерево для O(log n) доступа, но хранит ключи отсортированными. Используйте HashMap для быстрых lookup'ов; BTreeMap — когда нужно упорядоченную итерацию или range-запросы. entry().or_insert() — идиоматичный способ upsert — возвращает изменяемую ссылку на значение, вставляя значение по умолчанию при отсутствии.

rust
use std::collections::HashMap;
use std::collections::BTreeMap;

// HashMap: O(1) average lookup, unordered
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry("Bob".into()).or_insert(0); // insert if absent
let alice = scores.get("Alice"); // Some(&10)

// iterate (unordered)
for (name, score) in &scores {
    println!("{}: {}", name, score);
}

// BTreeMap: O(log n) lookup, sorted by key
let mut bt: BTreeMap<String, i32> = BTreeMap::new();
bt.insert("zebra".into(), 1);
bt.insert("apple".into(), 2);
// iterates in sorted order: apple, zebra

HashSet и BTreeSet

HashSet хранит уникальные значения с O(1) проверкой принадлежности. Операции над множествами (union, intersection, difference, symmetric_difference) возвращают итераторы. Используйте множества для дедупликации, проверки принадлежности и математических операций над множествами. BTreeSet — отсортированный эквивалент, поддерживаемый BTreeMap.

rust
use std::collections::HashSet;

let mut a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5].into_iter().collect();

a.insert(4);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));

// set operations
let union: HashSet<_> = a.union(&b).copied().collect();
let inter: HashSet<_> = a.intersection(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();
let sym: HashSet<_> = a.symmetric_difference(&b).copied().collect();

Кортежи и деструктуризация

Кортежи группируют фиксированное число значений потенциально разных типов. Доступ к полям через .0, .1 и т.д. Деструктуризация с let-паттернами идиоматична. Единичный тип () имеет одно значение () и используется как void в других языках. Кортежи часто используются для возврата нескольких значений из функций.

rust
// tuples can hold different types
let tup: (i32, f64, &str) = (42, 3.14, "hi");

// access by index
println!("{} {} {}", tup.0, tup.1, tup.2);

// destructuring
let (n, pi, s) = tup;
println!("{} {} {}", n, pi, s);

// nested
let ((a, b), c) = ((1, 2), 3);

// unit tuple (empty)
let unit: () = ();

// function returning multiple values
fn divmod(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}
let (q, r) = divmod(17, 5); // (3, 2)
04

Управление потоком

If / Else If / Else

В отличие от многих языков, 'if' в Rust — выражение, возвращающее значение. Это устраняет необходимость в тернарном операторе (condition ? a : b) — просто используйте if/else. Обе ветки должны возвращать одинаковый тип. Условие НЕ требует скобок, но тело должно быть блоком { }.

rust
let score = 85;

// if is an expression — returns a value
let grade = if score >= 90 {
    "A"
} else if score >= 80 {
    "B"
} else if score >= 70 {
    "C"
} else {
    "F"
};
println!("Grade: {}", grade);

// arms must return same type
// let bad = if true { 1 } else { "two" }; // ERROR!

Циклы: loop, while, for

loop создаёт бесконечный цикл — break выходит из него и может вернуть значение (break value). while проверяет условие перед каждой итерацией. for — наиболее распространённый цикл, итерирует по диапазонам, массивам, векторам, итераторам. Используйте 'continue' для перехода к следующей итерации. Диапазоны: a..b эксклюзивный, a..=b инклюзивный.

rust
// loop: infinite, use break to exit
let mut count = 0;
let result = loop {
    count += 1;
    if count == 10 {
        break count * 2; // break with value
    }
};
println!("{}", result); // 20

// while: condition-checked loop
let mut n = 5;
while n > 0 {
    println!("{}", n);
    n -= 1;
}

// for: iterate over anything with IntoIterator
for i in 0..5 { print!("{}", i); }      // 01234
for i in (1..=3).rev() { print!("{}", i); } // 321
for ch in "hello".chars() { print!("{}", ch); }

Match (сопоставление с образцом)

match — мощная конструкция сопоставления с образцом в Rust. Она должна быть исчерпывающей (все возможности покрыты) — используйте _ как catch-all. Паттерны поддерживают литералы, диапазоны (..=), or-паттерны (|), привязки и guards (if). match — выражение, возвращающее значение. Это идиоматичный способ обработки enum'ов вроде Option и Result.

rust
let coin = 25;
match coin {
    25 => println!("quarter"),
    10 => println!("dime"),
    5 => println!("nickel"),
    1 => println!("penny"),
    _ => println!("unknown"),  // catch-all (required)
}

// matching with bindings
let x = 3;
match x {
    1 | 2 => println!("one or two"),   // or-pattern
    3..=9 => println!("single digit"),  // range
    n if n % 2 == 0 => println!("even: {}", n), // guard
    _ => println!("other"),
}

// match is exhaustive — all cases must be covered

If Let и While Let

if let — синтаксический сахар для match, когда важен только один вариант. Менее многословен, но менее исчерпывающий, чем match — используйте, когда другие случаи не важны. while let циклически выполняется, пока паттерн совпадает, обычно используется с итераторами (next() возвращает Option). Ветвь else опциональна.

rust
// if let: shorthand for matching one pattern
let maybe: Option<i32> = Some(5);

// verbose: match
match maybe {
    Some(x) => println!("got {}", x),
    None => println!("nothing"),
}

// concise: if let
if let Some(x) = maybe {
    println!("got {}", x);
} else {
    println!("nothing");
}

// while let: loop while pattern matches
let mut iter = vec![1, 2, 3].into_iter();
while let Some(n) = iter.next() {
    println!("{}", n);
}

Break, Continue и метки

Метки (одинарная кавычка + имя) позволяют прерывать или продолжать внешние циклы из вложенных. Это существенно, когда нужно выйти из нескольких уровней цикла одновременно. Без меток break/continue влияют только на внутренний цикл. Метки — чистая альтернатива переменным-флагам.

rust
// continue: skip to next iteration
for i in 0..10 {
    if i % 2 == 0 { continue; }
    println!("{}", i); // prints odd numbers
}

// break: exit loop
for i in 0..10 {
    if i == 5 { break; }
    println!("{}", i); // prints 0..4
}

// labeled loops (for nested break/continue)
'outer: for i in 0..3 {
    for j in 0..3 {
        if i == 1 && j == 1 {
            break 'outer; // breaks the outer loop
        }
        println!("{} {}", i, j);
    }
}
05

Функции и замыкания

Определение функций

Функции используют ключевое слово 'fn'. Последнее выражение без точки с запятой — возвращаемое значение (expression). Добавление точки с запятой превращает его в инструкцию, возвращающую (). Используйте явный 'return' только для ранних выходов. Возвращаемый тип ! отмечает расходящиеся функции, которые никогда не возвращаются (бесконечные циклы, паники, выходы из процесса).

rust
// basic function with return type
fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon = expression = return value
}

// statements (with semicolon) return ()
fn greet(name: &str) {
    println!("Hi, {}", name);
    // implicit return ()
}

// explicit return
fn abs(x: i32) -> i32 {
    if x < 0 {
        return -x;  // early return needs 'return'
    }
    x  // tail expression
}

// diverging function (never returns)
fn forever() -> ! {
    loop {}
}

Параметры и аргументы

В Rust нет перегрузки функций или опциональных параметров (используйте дженерики или builder'ы). Выбирайте типы параметров внимательно: &T для чтения, &mut T для записи, T для передачи владения. Срезы (&[T]) — идиоматичный способ принимать последовательности переменной длины. Аргументы по умолчанию не поддерживаются — используйте паттерн builder или Option<T>.

rust
// immutable borrow
fn len(s: &String) -> usize { s.len() }

// mutable borrow
fn push(v: &mut Vec<i32>) { v.push(42); }

// take ownership
fn consume(s: String) { println!("{}", s); }

// multiple return via tuple
fn swap(a: i32, b: i32) -> (i32, i32) { (b, a) }

// variadic-ish via slices
fn sum(nums: &[i32]) -> i32 {
    nums.iter().sum()
}
println!("{}", sum(&[1, 2, 3, 4])); // 10

Замыкания

Замыкания — анонимные функции, которые могут захватывать окружение. Выводятся по использованию. По умолчанию захватывают по ссылке; 'move' форсирует передачу владения (существенно для потоков). Замыкания реализуют типажи Fn (заимствование), FnMut (изменяемое заимствование) или FnOnce (потребление), что позволяет передавать их как параметры функций.

rust
// closure syntax: |params| body
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 3

// type annotations (rarely needed)
let square = |x: i32| -> i32 { x * x };

// capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15

// move closure: takes ownership of captured vars
let name = String::from("Alice");
let greet = move || println!("Hi {}", name);
// name is now moved into greet
greet();

Функции высшего порядка и итераторы

Итераторы Rust ленивы — операции не выполняются, пока не вызван .collect() или другой потребляющий метод. Это обеспечивает абстракцию без накладных расходов: компилятор может оптимизировать цепочки методов итератора в эффективные циклы. Распространённые методы: map (преобразование), filter (отбор), fold (аккумуляция), take (ограничение), skip, enumerate, zip, flat_map.

rust
let nums = vec![1, 2, 3, 4, 5];

// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();

// filter: keep elements matching predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// fold/reduce: accumulate
let sum: i32 = nums.iter().sum();              // 15
let product: i32 = nums.iter().product();       // 120
let combined = nums.iter().fold(0, |acc, x| acc + x);

// chain multiple operations (lazy!)
let result: Vec<i32> = nums.iter()
    .filter(|&&x| x > 1)
    .map(|&x| x * x)
    .collect(); // [4, 9, 16, 25]

Указатели на функции и типажи

fn (нижний регистр) — тип указателя на функцию — без накладных расходов, но не может захватывать окружение. Для замыканий, которые захватывают, используйте границы дженериков <F: Fn(...)>. Fn заимствует, FnMut изменяемо заимствует, FnOnce потребляет. Указатели на функции полезны для хранения функций в структурах или передачи в C. Дженерики с границами Fn более гибки и всё ещё без накладных расходов при мономорфизации.

rust
// function pointer type
type MathFn = fn(i32, i32) -> i32;

fn add(a: i32, b: i32) -> i32 { a + b }
fn mul(a: i32, b: i32) -> i32 { a * b }

fn apply(f: MathFn, a: i32, b: i32) -> i32 {
    f(a, b)
}
println!("{}", apply(add, 3, 4)); // 7
println!("{}", apply(mul, 3, 4)); // 12

// generic over Fn trait (accepts closures too)
fn apply_fn<F: Fn(i32, i32) -> i32>(f: F, a: i32, b: i32) -> i32 {
    f(a, b)
}
let closure = |a, b| a - b;
println!("{}", apply_fn(closure, 10, 3)); // 7
06

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

Правила владения

Владение — основная система управления памятью Rust — без сборщика мусора. При присваивании значения в куче (String, Vec) владение ПЕРЕДАЁТСЯ, и старая переменная становится недействительной. Стековые типы (i32, f64, bool, char, кортежи из Copy-типов) реализуют Copy и дублируются. Это устраняет use-after-free и double-free при компиляции.

rust
// Rule 1: Each value has ONE owner
let s1 = String::from("hello");
let s2 = s1;  // s1's ownership MOVED to s2
// println!("{}", s1); // ERROR: s1 is invalid after move

// Rule 2: When owner goes out of scope, value is dropped
{
    let s = String::from("temp");
    // s is valid here
} // s is automatically dropped (memory freed)

// Rule 3: Copy types (i32, bool, char, etc.) are copied, not moved
let a = 5;
let b = a;  // a is copied, both valid
println!("{} {}", a, b); // OK

Заимствование и ссылки

Заимствование позволяет использовать значение без владения. &T создаёт неизменяемую ссылку — их может быть много одновременно. &mut T создаёт изменяемую ссылку — но только ОДНА изменяемая ссылка ИЛИ любое количество неизменяемых, никогда оба. Это предотвращает гонки данных при компиляции. Ссылки всегда должны указывать на корректные данные (без висячих указателей).

rust
// &T: immutable borrow (read-only, multiple allowed)
fn calc_len(s: &String) -> usize {
    s.len()
    // s goes out of scope but is NOT dropped (we don't own it)
}
let s = String::from("hello");
let len = calc_len(&s);  // borrow s, don't move it
println!("'{}' has length {}", s, len); // s still valid

// &mut T: mutable borrow (exclusive, only ONE at a time)
fn push_world(s: &mut String) {
    s.push_str(", world");
}
let mut s2 = String::from("hello");
push_world(&mut s2);
println!("{}", s2); // hello, world

Ссылочные срезы

Срезы — ссылки на непрерывную часть коллекции. Это 'толстые указатели', содержащие указатель и длину. Строковые срезы (&str) позволяют функциям принимать и String, и строковые литералы. Срезы массивов (&[T]) работают с любой непрерывной последовательностью. Срезы заимствуют базовые данные, предотвращая их изменение или уничтожение, пока срез существует.

rust
// string slice: &str
let s = String::from("hello world");
let hello: &str = &s[0..5];   // "hello"
let world: &str = &s[6..];    // "world"
let full: &str = &s[..];      // "hello world"

// array slice: &[T]
let arr = [1, 2, 3, 4, 5];
let mid: &[i32] = &arr[1..4]; // [2, 3, 4]

// function accepting slices (idiomatic)
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

Времена жизни

Времена жизни сообщают компилятору, как долго действительны ссылки. Аннотация 'a не меняет времена жизни — она описывает отношения. 'static — специальное время жизни, действующее всю программу (строковые литералы имеют его). Большая часть кода использует ellision времён жизни (компилятор выводит). Явные времена жизни нужны, когда: функция возвращает ссылку или структура содержит ссылку.

rust
// explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y's lifetimes

let s1 = String::from("long string");
let s2 = String::from("hi");
let result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);

// struct holding references needs lifetime
struct Excerpt<'a> {
    part: &'a str,
}
let novel = String::from("call me Ishmael. years ago...");
let first_sentence = novel.split('.').next().unwrap();
let ex = Excerpt { part: first_sentence };

Умные указатели

Box<T> перемещает данные в кучу (один владелец). Rc<T> обеспечивает совместное владение через подсчёт ссылок (только однопоточно). Arc<T> — потокобезопасная версия с использованием атомарных операций. RefCell<T> переносит проверку заимствования в runtime, позволяя мутацию через общие ссылки (внутренняя изменяемость). Используйте Box для рекурсивных типов, Rc/Arc для графовых структур, RefCell когда нужно изменять общие данные.

rust
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;

// Box<T>: heap allocation, single owner
let b = Box::new(5); // 5 lives on heap
println!("{}", b);   // dereferenced automatically

// Rc<T>: reference counting, multiple owners (single-threaded)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments ref count
println!("count: {}", Rc::strong_count(&a)); // 2

// Arc<T>: atomic Rc, thread-safe
let arc = Arc::new(vec![1, 2, 3]);

// RefCell<T>: interior mutability (runtime borrow check)
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // mutable borrow checked at runtime
07

Структуры, Enum'ы и типажи

Определение структур

Структуры группируют связанные поля. Структуры с именованными полями наиболее распространены. Кортежные структуры полезны, когда имена полей не имеют смысла (Color, Point). Unit-структуры не имеют данных и используются для реализации типажей. Синтаксис .. копирует неуказанные поля из другого экземпляра. Структуры размещаются в стеке, если не содержат кучные типы (String, Vec, Box).

rust
// named-field struct
struct User {
    name: String,
    age: u32,
    active: bool,
}

let u = User {
    name: String::from("Alice"),
    age: 30,
    active: true,
};

// field init shorthand
let name = String::from("Bob");
let u2 = User { name, age: 25, active: true };

// update syntax (copy remaining fields from another)
let u3 = User { age: 40, ..u2 };

// tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);

// unit struct (no fields, useful for traits)
struct AlwaysEqual;

Методы с impl

Методы находятся в блоках impl. &self заимствует неизменяемо, &mut self заимствует изменяемо, self забирает владение (потребление). Ассоциированные функции (без параметра self) подобны статическим методам — вызываются Type::function(). Self — алиас для типа. Допускаются несколько блоков impl, полезно для разделения методов по ответственности или условной компиляции.

rust
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // associated function (constructor, no &self)
    fn new(w: f64, h: f64) -> Self {
        Rectangle { width: w, height: h }
    }

    // method (borrows self)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // mutable method
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // consuming method (takes ownership)
    fn into_square(self) -> Rectangle {
        let side = (self.width + self.height) / 2.0;
        Rectangle { width: side, height: side }
    }
}

let mut r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area()); // 50
r.scale(2.0);
let sq = r.into_square(); // r consumed

Enum'ы и сопоставление с образцом

Enum'ы в Rust — алгебраические типы данных — каждый вариант может нести разные данные. Это делает их гораздо мощнее C enum'ов. Сопоставление с образцом через match деконструирует варианты и извлекает данные. Используйте enum'ы, когда значение может быть одной из нескольких различных форм. Макрос matches! — сокращение для сопоставления с одним паттерном, возвращающее bool.

rust
// enum with data (algebraic data type)
enum Message {
    Quit,                          // no data
    Move { x: i32, y: i32 },       // named fields
    Write(String),                 // tuple variant
    ChangeColor(i32, i32, i32),    // tuple variant
}

// pattern matching with destructuring
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("RGB({}, {}, {})", r, g, b),
    }
}

process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));

// enums can have methods too
impl Message {
    fn is_quit(&self) -> bool {
        matches!(self, Message::Quit)
    }
}

Option и Result

Option<T> заменяет null — вы должны явно обработать случай None, устраняя баги в стиле NullPointerException. Result<T, E> — для операций, которые могут завершиться неудачей. Оба имеют богатые методы: map (преобразование), and_then (цепочка), unwrap_or (значение по умолчанию), is_some/is_ok (проверка). Оператор ? для Result автоматически распространяет ошибки. Эти два типа — основа обработки ошибок Rust.

rust
// Option<T>: Some(value) or None (replaces null)
fn find_user(id: i32) -> Option<String> {
    if id == 1 { Some(String::from("Alice")) }
    else { None }
}

let user = find_user(1);
match user {
    Some(name) => println!("Found: {}", name),
    None => println!("Not found"),
}

// convenient methods
let name = find_user(1).unwrap_or("Anonymous".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).and_then(|n| Some(n.len()));

// Result<T, E>: Ok(value) or Err(error)
fn parse_num(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse()
}

match parse_num("42") {
    Ok(n) => println!("Parsed: {}", n),
    Err(e) => println!("Error: {}", e),
}

Типажи и границы типажей

Типажи определяют общее поведение (как интерфейсы в других языках). Типы реализуют типажи через 'impl Trait for Type'. Типажи могут иметь реализации методов по умолчанию. Границы типажей (<T: Trait>) ограничивают дженерики типами, реализующими определённые типажи. Предложение 'where' улучшает читаемость для сложных границ. Типажи обеспечивают полиморфизм как через статическую диспетчеризацию (дженерики), так и через динамическую (trait-объекты &dyn Trait).

rust
// define a trait (interface)
trait Summary {
    fn summarize(&self) -> String;

    // default method
    fn author(&self) -> String {
        String::from("Unknown")
    }
}

struct Article { title: String, content: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
    // author() uses default implementation
}

let a = Article {
    title: "Rust".into(),
    content: "Great".into(),
};
println!("{}", a.summarize());
println!("Author: {}", a.author()); // Unknown

// generic with trait bound
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// multiple bounds with +, or where clause
fn display<T: Summary + std::fmt::Display>(item: &T) {}
// or: fn display<T>(item: &T) where T: Summary + std::fmt::Display {}

Derive-макросы и trait-объекты

#[derive(...)] авто-реализует распространённые типажи: Debug (отладочная печать), Clone (глубокая копия), PartialEq/Eq (сравнение ==), Hash (для ключей HashMap), Copy (стековое копирование вместо перемещения). Trait-объекты (&dyn Trait или Box<dyn Trait>) обеспечивают runtime-полиморфизм через vtable с небольшими затратами производительности. Используйте дженерики для статической диспетчеризации (без накладных расходов), когда возможно, trait-объекты — для гетерогенных коллекций.

rust
// derive common traits automatically
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();          // Clone
println!("{:?}", p1);          // Debug
println!("{}", p1 == p2);      // PartialEq -> true

// trait object: dynamic dispatch
trait Animal {
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;

impl Animal for Dog { fn sound(&self) -> String { "Woof".into() } }
impl Animal for Cat { fn sound(&self) -> String { "Meow".into() } }

// Vec of trait objects (dynamic dispatch via vtable)
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];
for a in &animals {
    println!("{}", a.sound());
}
08

Обработка ошибок

Result и оператор ?

Оператор ? — идиоматичный способ распространения ошибок. На Ok(v) он разворачивается в v. На Err(e) он немедленно возвращает Err(e) из функции. ? также преобразует типы ошибок через типаж From, поэтому функции, возвращающие Box<dyn Error>, могут использовать ? с любым типом ошибки. На Option ? возвращает None раньше. Это делает обработку ошибок лаконичной без потери безопасности.

rust
use std::fs;
use std::io;
use std::num::ParseIntError;

// ? propagates errors: if Err, return early; if Ok, unwrap
fn read_config(path: &str) -> Result<i32, io::Error> {
    let content = fs::read_to_string(path)?; // ? on io::Result
    Ok(content.len() as i32)
}

// ? converts error types via From
fn parse_and_read(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;     // io::Error -> Box
    let n: i32 = content.trim().parse()?;         // ParseIntError -> Box
    Ok(n)
}

// ? on Option too
fn first_char(s: &str) -> Option<char> {
    s.lines().next()?.chars().next()
}

Panic vs Result

Используйте panic! для невосстановимых ошибок (баги, нарушенные инварианты) — это указывает на ошибку программирования. Используйте Result для ожидаемых, восстановимых неудач (пользовательский ввод, файловый ввод-вывод, сеть). unwrap()/expect() паникуют при ошибке — допустимо в тестах, прототипах или когда можно доказать корректность значения. В production-коде предпочитайте надлежащую обработку ошибок с ? и match.

rust
// panic: unrecoverable error, crashes the program
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Division by zero!"); // unwinds stack
    }
    a / b
}

// Result: recoverable error, caller decides
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}

// unwrap/expect: panic on Err (use sparingly)
let n: i32 = "42".parse().unwrap();       // panics on error
let n2: i32 = "42".parse().expect("valid number"); // panic with msg

// when to panic vs Result:
// panic: bugs, invariant violations, impossible states
// Result: expected failures (file not found, parse error)

Пользовательские типы ошибок

Пользовательские типы ошибок дают типобезопасную, структурированную обработку ошибок. Реализуйте Display (человекочитаемое) и Error (для цепочки источников). Реализуйте From для каждой базовой ошибки, чтобы ? конвертировал автоматически. Библиотеки вроде thiserror (derive-макрос) или anyhow (динамические боксы ошибок) уменьшают шаблонность. Используйте thiserror для библиотек, anyhow для приложений.

rust
use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
}

// implement Display (required by Error)
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(name) => write!(f, "Not found: {}", name),
        }
    }
}

// implement Error
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            _ => None,
        }
    }
}

// From impls for ? to work automatically
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

Сопоставление и комбинирование Results

Комбинаторы Result позволяют функциональную обработку ошибок без match. map преобразует Ok-значение, map_err преобразует ошибку, and_then цепляет операции, которые могут завершиться неудачей (flatMap). unwrap_or предоставляет значение по умолчанию при ошибке. is_ok/is_err проверяют без потребления. Эти методы делают цепочки обработки ошибок читаемыми и избегают глубокой вложенности.

rust
// match on Result
let result: Result<i32, &str> = Ok(42);
match result {
    Ok(n) => println!("Got: {}", n),
    Err(e) => println!("Error: {}", e),
}

// combinators
let r1: Result<i32, &str> = Ok(5);
let doubled = r1.map(|n| n * 2);           // Ok(10)
let mapped_err = r1.map_err(|e| format!("{}", e));

// and_then: chain fallible operations
let r2 = r1.and_then(|n| if n > 0 { Ok(n * 2) } else { Err("negative") });

// unwrap_or family
let val = r1.unwrap_or(0);        // 5 or 0
let val2 = r1.unwrap_or_default(); // 5 or T::default()
let val3 = r1.unwrap_or_else(|_| 0); // lazy default

// check variants
println!("{}", r1.is_ok());  // true
println!("{}", r1.is_err()); // false

Преобразование ошибок с From

Оператор ? использует From для преобразования ошибок. Box<dyn Error> реализует From для всех стандартных ошибок, что делает его удобным catch-all. Крейт anyhow предоставляет anyhow::Result, добавляющий контекст (например, .context("failed to read config")?). Для библиотек определите конкретный enum ошибок с thiserror; для приложений используйте anyhow для простоты.

rust
use std::fs;
use std::num::ParseIntError;

// Without From: manual conversion needed
fn manual(path: &str) -> Result<i32, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| e.to_string())?;  // manual convert
    let n: i32 = content.trim().parse()
        .map_err(|e| e.to_string())?;  // manual convert
    Ok(n)
}

// With Box<dyn Error>: auto-converts via From
fn boxed(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;  // auto-converts
    let n: i32 = content.trim().parse()?;      // auto-converts
    Ok(n)
}

// anyhow::Result (from anyhow crate) is ergonomic for apps
// fn anyhow_fn() -> anyhow::Result<i32> {
//     let n: i32 = something()?; // any error auto-converted
//     Ok(n)
// }
09

Модули и крейты

Система модулей

Система модулей Rust организует код. 'mod' объявляет модуль (инлайн или через файл). 'pub' делает элементы публичными (по умолчанию приватные). 'use' создаёт ярлыки к путям. 'pub use' реэкспортирует элементы (полезно для API-дизайна). Файловая система отражает дерево модулей: mod network может быть src/network.rs или src/network/mod.rs. Корень крейта — lib.rs (библиотеки) или main.rs (бинарники).

rust
// mod.rs or mod declaration in lib.rs/main.rs
// File: src/lib.rs
mod network {
    pub mod connection {
        pub fn connect() -> bool { true }
        fn disconnect() {} // private
    }
}

// use: bring paths into scope
use network::connection::connect;

// calling with full path
fn main() {
    network::connection::connect();
    connect(); // after 'use'
}

// re-export with pub use
pub use network::connection::connect as open_connection;

// nested file: src/network/connection.rs
// mod network; in lib.rs loads src/network/mod.rs
// which can have: pub mod connection;

Приватность и видимость

Приватность в Rust областна модулями. Элементы приватны по умолчанию — доступны только внутри определяющего модуля и потомков. 'pub' делает их публичными. 'pub(crate)' ограничивает текущим крейтом (полезно для внутренней реализации библиотек). 'pub(super)' ограничивает родительским модулем. Поля структур имеют индивидуальную видимость — pub-структура может иметь приватные поля, требуя конструктор.

rust
mod my_module {
    // private by default
    fn internal_helper() {}

    // public: accessible from outside
    pub fn public_api() {
        internal_helper(); // can call private within module
    }

    // pub(crate): visible within this crate only
    pub(crate) fn crate_wide() {}

    // pub(super): visible to parent module
    pub(super) fn parent_visible() {}

    // struct fields are private even if struct is pub
    pub struct Config {
        pub name: String,    // public field
        secret: String,      // private field
    }

    // enum variants inherit the enum's visibility
    pub enum Status {
        Active,  // public because Status is public
        Inactive,
    }
}

Инструкции use и алиасы

Инструкции use приносят элементы в область видимости, уменьшая многословность путей. Сгруппированные импорты (use std::io::{self, Read}) чище, чем несколько строк. Типажи должны быть в области видимости для использования их методов — поэтому иногда нужно 'use std::io::Read', даже если вы не ссылаетесь на Read по имени. Глобальные импорты (*) не рекомендуются, кроме preludes.

rust
// basic use
use std::collections::HashMap;
use std::fs::read_to_string;

// grouped use
use std::io::{self, Read, Write, BufRead};
// equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;

// aliasing with 'as'
use std::collections::HashMap as Map;

// glob import (use sparingly)
use std::prelude::v1::*;

// bringing trait methods into scope
use std::io::Read; // now .read() method is available
let mut f = std::fs::File::open("x")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?; // Read trait method

Cargo и внешние крейты

Cargo — пакетный менеджер и система сборки Rust. Зависимости идут в Cargo.toml под [dependencies]. Функции включают опциональную функциональность (уменьшает время компиляции/размер бинарника). 'cargo add' авто-редактирует Cargo.toml. Крейты публикуются на crates.io. Edition (2021) управляет языковыми функциями. Cargo обрабатывает компиляцию, тестирование, документацию и публикацию.

rust
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"

# in Rust code
use serde::{Serialize, Deserialize};
use rand::Rng;

#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }

fn main() {
    let mut rng = rand::thread_rng();
    let n: i32 = rng.gen_range(1..=100);
    println!("{}", n);
}

# CLI commands:
# cargo new my_app      # create new project
# cargo build           # compile
# cargo run             # compile + run
# cargo test            # run tests
# cargo add serde       # add dependency
# cargo update          # update dependencies

Тестирование

Тесты используют атрибут #[test]. assert_eq!/assert_ne! сравнивают значения. #[should_panic] проверяет возникновение паники. Тесты могут возвращать Result для assert'ов на основе ошибок. #[cfg(test)] гарантирует, что модуль тестов компилируется только во время тестирования. Unit-тесты живут рядом с кодом; интеграционные тесты — в директории tests/. Запуск через 'cargo test'. Используйте #[ignore] для пропуска flaky-тестов.

rust
// Unit tests in same file (convention: tests module)
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 3), 6);
    }

    #[test]
    fn test_with_message() {
        let result = add(1, 1);
        assert_eq!(result, 2, "Expected 2, got {}", result);
    }

    #[test]
    #[should_panic(expected = "division by zero")]
    fn test_panic() {
        panic!("division by zero");
    }

    #[test]
    fn test_result() -> Result<(), String> {
        if add(2, 2) == 4 { Ok(()) }
        else { Err(String::from("math is broken")) }
    }
}

// Integration tests: tests/integration_test.rs
// Run: cargo test
10

Конкурентность и файловый ввод-вывод

Потоки

std::thread::spawn создаёт OS-потоки. Замыкание должно быть 'move', если оно захватывает переменные, передавая владение потоку (предотвращая use-after-free). join() блокируется до завершения потока, возвращая Result. Система владения Rust предотвращает гонки данных при компиляции — нельзя разделять изменяемые данные между потоками без синхронизации (Arc<Mutex<T>>).

rust
use std::thread;
use std::time::Duration;

// spawn a thread
let handle = thread::spawn(|| {
    for i in 0..5 {
        println!("spawned thread: {}", i);
        thread::sleep(Duration::from_millis(10));
    }
});

// main thread continues
for i in 0..3 {
    println!("main thread: {}", i);
}

// wait for spawned thread to finish
handle.join().unwrap();

// move closure: transfer ownership to thread
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
    println!("got data: {:?}", data); // data moved here
});
h.join().unwrap();

Каналы (передача сообщений)

Каналы обеспечивают потоковое взаимодействие через передачу сообщений (девиз Go: 'Делись памятью через общение'). mpsc позволяет несколько отправителей (tx.clone()), но одного получателя. send() возвращает Result (Err если получатель удалён). Получатель реализует Iterator, поэтому for-циклы работают естественно. Для нескольких получателей используйте crossbeam-channel или async-каналы. Передача сообщений избегает сложности разделяемого изменяемого состояния.

rust
use std::sync::mpsc;
use std::thread;

// mpsc: multiple producer, single consumer
let (tx, rx) = mpsc::channel();

// clone transmitter for multiple producers
let tx2 = tx.clone();

thread::spawn(move || {
    let vals = vec!["a", "b", "c"];
    for v in vals {
        tx.send(v).unwrap();
    }
});

thread::spawn(move || {
    tx2.send("from tx2").unwrap();
});

// receiver is an iterator
for received in rx {
    println!("Got: {}", received);
}

Mutex и Arc (разделяемое состояние)

Arc (Atomic Reference Counted) обеспечивает совместное владение между потоками (потокобезопасный Rc). Mutex предоставляет эксклюзивный доступ — lock() блокируется до получения, возвращает MutexGuard, освобождающий блокировку при drop. RwLock разрешает несколько читателей или одного писателя. Комбинация Arc<Mutex<T>> — стандартный паттерн для разделяемого изменяемого состояния. unwrap() на lock() обрабатывает poison (поток паниковал, удерживая блокировку).

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

// Arc: thread-safe reference counting
// Mutex: mutual exclusion lock
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        // lock() returns MutexGuard, auto-unlocks when dropped
        let mut num = counter.lock().unwrap();
        *num += 1;
    }); // lock released here
    handles.push(handle);
}

for h in handles { h.join().unwrap(); }

println!("Result: {}", *counter.lock().unwrap()); // 10

// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let data = RwLock::new(5);
let r1 = data.read().unwrap();  // shared read
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // would block until r1, r2 dropped

Файловый ввод-вывод

fs::read_to_string удобен для небольших файлов. Для больших файлов используйте BufReader/BufWriter для уменьшения системных вызовов. Типажи Read/Write предоставляют низкоуровневые байтовые операции. BufRead добавляет lines() и read_line() для текста. Всегда обрабатывайте ошибки через ? (файлы могут отсутствовать, отказ в правах, переполнение диска). flush() гарантирует, что буферизованные данные достигнут ОС (но не обязательно диска).

rust
use std::fs;
use std::io::{Read, Write, BufReader, BufWriter};
use std::fs::File;

// read entire file
let content = fs::read_to_string("file.txt")?;
fs::write("output.txt", "Hello")?;

// buffered read (efficient for large files)
let file = File::open("file.txt")?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;

// line by line
use std::io::BufRead;
let file = File::open("file.txt")?;
for line in BufReader::new(file).lines() {
    println!("{}", line?);
}

// buffered write
let file = File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "Line 1")?;
writeln!(writer, "Line 2")?;
writer.flush()?; // ensure written to disk

Async/Await (Tokio)

Async/await обеспечивает эффективную конкурентность без OS-потоков — задачи работают на пуле потоков и уступают в точках .await. tokio — самая популярная async-runtime. async fn возвращает Future, который нужно .await'ить. tokio::join! запускает future конкурентно и ждёт все. tokio::select! соревнует future. Async идеален для I/O-bound работы (сеть, файлы); используйте потоки для CPU-bound работы. .await не блокирует поток — он возвращает управление runtime.

rust
use tokio::{fs, task, time};
use std::time::Duration;

// async fn returns a Future
async fn fetch_data() -> String {
    time::sleep(Duration::from_secs(1)).await;
    String::from("data")
}

// spawn concurrent tasks
#[tokio::main]
async fn main() {
    // run tasks concurrently
    let (a, b, c) = tokio::join!(
        fetch_data(),
        fetch_data(),
        fetch_data()
    );
    println!("{} {} {}", a, b, c);

    // spawn a background task
    let handle = tokio::spawn(async {
        let data = fs::read_to_string("file.txt").await.unwrap();
        println!("Read {} bytes", data.len());
    });
    handle.await.unwrap();

    // select: first to complete wins
    tokio::select! {
        val = fetch_data() => println!("Got: {}", val),
        _ = time::sleep(Duration::from_millis(500)) => {
            println!("Timeout!");
        }
    }
}
11

Углублённое изучение времён жизни

Именованные времена жизни в функциях

Времена жизни — способ компилятора отслеживать действительность ссылок. 'a — дженерик-параметр времени жизни — он не меняет runtime-поведение, только compile-time проверку. Когда функция принимает несколько ссылок и возвращает одну, вы должны аннотировать времена жизни, чтобы компилятор знал, что возвращаемая ссылка не переживёт свои входные данные. Это предотвращает висячие указатели при компиляции.

rust
// Lifetimes tell the compiler how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y

let s1 = String::from("long string");
let result;
{
    let s2 = String::from("hi");
    result = longest(s1.as_str(), s2.as_str());
    // result valid here
    println!("{}", result);
}
// result NOT valid here — s2 is dropped

Правила ellision времён жизни

Правила ellion позволяют опускать явные аннотации времён жизни в распространённых случаях. Правило 1 назначает различное время жизни каждому ссылочному параметру. Правило 2 назначает это время жизни выходному, если есть ровно одна входная ссылка. Правило 3 применяется к методам — выход получает время жизни &self. Когда ни одно из правил не разрешает все ссылки, нужно писать явные времена жизни. Большая часть идиоматичного кода Rust редко требует явных аннотаций времён жизни.

rust
// The compiler applies 3 elision rules automatically:
// 1. Each input reference gets its own lifetime
// 2. If one input lifetime, output gets that lifetime
// 3. If &self/&mut self, output gets self's lifetime

// These DON'T need explicit annotations (rule 2):
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' { return &s[0..i]; }
    }
    &s[..]
}

// This NEEDS annotation (multiple inputs, rule doesn't apply):
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

Время жизни 'static

'static — самое длинное время жизни — длится всю программу. Все строковые литералы имеют это время жизни, так как они встроены в бинарник. Когда вы видите T: 'static как границу, это не значит, что T должен жить вечно — это значит, что T не должен содержать ссылок короче 'static (т.е. owned типы вроде String, Vec, i64 всегда удовлетворяют этому). Потоки требуют 'static, так как могут пережить вызывающую функцию.

rust
// 'static means the reference lives for the entire program
let s: &'static str = "I live forever";
// All string literals are &'static str

// Static variables (global, program-wide)
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);

// 'static in bounds: T: 'static means T contains no
// non-static references (owned data always satisfies this)
fn spawn_thread<T: Send + 'static>(t: T) {
    std::thread::spawn(move || {
        drop(t);
    });
}

Времена жизни в структурах

Когда структура содержит ссылку (не owned тип), ей нужен параметр времени жизни, чтобы объявить, как долго действительна эта ссылка. Экземпляр структуры не может пережить данные, которые он заимствует. Это распространено для zero-copy парсеров, итераторов по заимствованным данным и views. Время жизни должно быть объявлено и в структуре, и в блоке impl. Предпочитайте owned данные (String, Vec), если нет конкретной причины заимствовать.

rust
// Structs holding references need lifetime annotations
struct Parser<'a> {
    text: &'a str,
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(text: &'a str) -> Self {
        Parser { text, pos: 0 }
    }
    fn peek(&self) -> Option<char> {
        self.text[self.pos..].chars().next()
    }
    fn advance(&mut self) {
        self.pos += 1;
    }
}

// The struct cannot outlive the text it borrows
let text = String::from("hello");
let mut p = Parser::new(&text);
p.advance();

Множественные времена жизни и подтипизация

Функции с несколькими ссылками, взаимодействующими по-разному, требуют нескольких параметров времён жизни. Подтипизация времён жизни означает, что более длинное время жизни может заместить более короткое (ковариантность) — &'static может использоваться везде, где ожидается &'a. Поэтому 'static — подтип всех времён жизни. Используйте несколько времён жизни, когда время жизни выхода зависит только от некоторых входов, давая компилятору больше гибкости и вызывающему меньше ограничений.

rust
// Multiple lifetime parameters
fn parse<'src, 'ctx>(src: &'src str, ctx: &'ctx Context) -> &'src str {
    // returns something tied to src, not ctx
    src
}

// 'static: 'a is always true (static outlives everything)
fn static_ref<'a>(_: &'a str) -> &'static str {
    "constant"  // 'static coerces to 'a
}

// Variance: &'long can be used where &'short is expected
// (covariance) — longer lifetime is a subtype
fn use_ref<'short>(r: &'short str) {}
let long: &'static str = "hi";
use_ref(long);  // OK: 'static coerces to any 'short
12

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

Box<T> — выделение в куче

Box<T> — простейший умный указатель Rust — выделяет значение в куче с единственным владением. Используйте для рекурсивных типов (чей размер не может быть известен при компиляции), больших значений, которые не хотите перемещать по стеку, и trait-объектов (Box<dyn Trait>) для динамической диспетчеризации. Box deref'ится к T, поэтому ведёт себя как внутреннее значение. Он имеет практически нулевые накладные расходы помимо выделения в куче.

rust
// Box moves data to the heap (single owner)
let b = Box::new(5);
println!("{}", b);  // derefs automatically

// Recursive types NEED Box (size unknown at compile time)
enum List {
    Cons(i32, Box<List>),
    Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

// Trait objects (dynamic dispatch)
let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }

// Box has zero runtime overhead when dereferencing

Rc<T> — подсчёт ссылок (однопоточно)

Rc<T> (Reference Counted) обеспечивает множественное владение для однопоточных сценариев. Rc::clone инкрементирует счётчик ссылок вместо копирования данных — дешёвое копирование указателя. Значение уничтожается, когда последний Rc уничтожается. Используйте Rc для общих узлов графа, родительско-дочерних деревьев или любой структуры, где несколько частей должны владеть одними данными. Rc НЕ потокобезопасен — используйте Arc для многопоточности. Rc неизменяем — нельзя изменять через него напрямую.

rust
use std::rc::Rc;

// Rc allows multiple owners via reference counting
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a);  // increments count, doesn't copy
let c = Rc::clone(&a);

println!("count = {}", Rc::strong_count(&a));  // 3
// Data dropped when count reaches 0

// Shared graph/tree structures
struct Node {
    children: Vec<Rc<Node>>,
    value: i32,
}
let leaf = Rc::new(Node { children: vec![], value: 1 });
let branch = Rc::new(Node {
    children: vec![Rc::clone(&leaf)],
    value: 2,
});

Arc<T> — атомарный подсчёт ссылок (потокобезопасный)

Arc<T> (Atomically Reference Counted) — потокобезопасный аналог Rc. Использует атомарные операции для подсчёта ссылок, что делает его безопасным для разделения между потоками. Компромисс — немного больше накладных расходов, чем у Rc. Используйте Arc, когда нужно совместное владение между несколькими потоками. Для изменения общих данных комбинируйте Arc с Mutex (для эксклюзивного доступа) или RwLock (для доступа с интенсивным чтением). Arc::clone дешев — просто инкрементирует атомарный счётчик.

rust
use std::sync::Arc;
use std::thread;

// Arc: thread-safe version of Rc (atomic ref counting)
let data = Arc::new(vec![1, 2, 3, 4, 5]);

let handles: Vec<_> = (0..3).map(|i| {
    let data = Arc::clone(&data);  // atomic increment
    thread::spawn(move || {
        println!("Thread {} sees: {:?}", i, *data);
    })
}).collect();

for h in handles { h.join().unwrap(); }

// Arc is slightly slower than Rc due to atomic operations
// Only use Arc when actually sharing across threads

RefCell<T> — внутренняя изменяемость

RefCell<T> обеспечивает внутреннюю изменяемость — можно изменять через общую ссылку, с правилами заимствования, проверяемыми в runtime, а не при компиляции. borrow() возвращает неизменяемую ссылку, borrow_mut() — изменяемую. Нарушение правил (например, две изменяемые заимствования) вызывает панику в runtime. Используйте RefCell, когда компилятор не может доказать безопасность заимствования (например, графовые структуры, мок-объекты в тестах). Rc<RefCell<T>> — классический паттерн для изменяемых общих однопоточных данных.

rust
use std::cell::RefCell;

// RefCell moves borrow checking to RUNTIME
let cell = RefCell::new(vec![1, 2, 3]);

// Multiple immutable borrows OR one mutable borrow
{
    let mut borrowed = cell.borrow_mut();
    borrowed.push(4);
}  // borrow released here

{
    let r1 = cell.borrow();     // OK
    let r2 = cell.borrow();     // OK — multiple immutable
    println!("{:?} {:?}", r1, r2);
}

// cell.borrow_mut() while r1 alive → PANIC at runtime
// Combine with Rc: Rc<RefCell<T>> for mutable shared graphs

Weak<T> — разрыв циклов ссылок

Weak<T> — не владеющая ссылка, не влияющая на счётчик сильных ссылок. Это существенно для разрыва циклов ссылок: если родитель владеет детьми (Rc), а дети владеют родителем (Rc), ни один не будет освобождён — утечка памяти. Решение — сделать обратную ссылку Weak. upgrade() возвращает Option<Rc<T>> — None, если значение уже уничтожено. Используйте Weak для ссылок child→parent, кэшей и паттернов observer, где не хотите удерживать данные живыми.

rust
use std::rc::{Rc, Weak, RefCell};

// Weak references don't contribute to the strong count
// — prevents memory leaks in cycles
struct Node {
    parent: RefCell<Weak<Node>>,        // weak: child → parent
    children: RefCell<Vec<Rc<Node>>>,   // strong: parent → children
}

let leaf = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

// Upgrade returns Option — parent may already be dropped
if let Some(p) = leaf.parent.borrow().upgrade() {
    println!("parent exists");
}
13

Trait-объекты и динамическая диспетчеризация

dyn Trait — динамическая диспетчеризация

dyn Trait включает динамическую диспетчеризацию — конкретный тип стирается при компиляции, а вызовы методов проходят через vtable в runtime. Это позволяет хранить гетерогенные типы в одной коллекции (Vec<Box<dyn Animal>>). Компромисс: небольшая runtime-стоимость (vtable-косвенность, нет inlining) и тип не может быть известен при компиляции. Используйте trait-объекты, когда набор конкретных типов неизвестен при компиляции или нужно группировать разные типы.

rust
trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Woof".into() }
}
impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Meow".into() }
}

// dyn Trait = erased type, runtime dispatch via vtable
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog { name: "Rex".into() }),
    Box::new(Cat { name: "Whiskers".into() }),
];
for a in &animals {
    println!("{} says {}", a.name(), a.sound());
}

Правила объектной безопасности

Типаж объектно-безопасен, только если компилятор может построить для него vtable. Два правила: (1) методы не должны возвращать Self (конкретный тип стёрт, поэтому не может быть известен), и (2) методы не должны иметь дженерик-параметры типов (vtable требовалась бы запись для каждого возможного типа). Типажи с Sized как supertrait также не объектно-безопасны. Если нужна объектная безопасность, рефакторите Self-возвращающие методы на Box<dyn Trait> или используйте отдельную фабричную функцию.

rust
// Object-safe traits CAN be used as dyn Trait:
trait Draw {
    fn draw(&self);  // OK: &self, no generics
}

// NOT object-safe:
trait Bad {
    fn create() -> Self;        // returns Self — needs known type
    fn process<T>(&self, x: T); // generic method — vtable can't cover all T
    const SIZE: usize;          // associated const (sometimes OK)
}

// Workaround for Self returns — use a factory or Box<Self>
trait GoodFactory {
    fn new_boxed() -> Box<dyn GoodFactory>;
}

// Sized bound makes trait non-object-safe
// trait Foo: Sized {}  // NOT object-safe

Trait-объекты vs дженерики

Дженерики используют мономорфизацию — компилятор генерирует отдельную копию функции для каждого конкретного типа, обеспечивая статическую диспетчеризацию и полную оптимизацию (inlining). Это не имеет runtime-стоимости, но увеличивает размер бинарника. Trait-объекты (dyn) используют одну функцию с vtable-lookup — меньший бинарник, но небольшая runtime-стоимость на вызов. Выбирайте дженерики, когда важна производительность и набор типов мал/известен; выбирайте dyn для гетерогенных коллекций или когда все типы неизвестны заранее.

rust
// Generics: monomorphization, static dispatch, zero overhead
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
// Each type instantiation creates a separate function:
// max_generic::<i32>, max_generic::<f64>, etc.

// Trait objects: dynamic dispatch, single function
fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> bool {
    // can't return — don't know size at compile time
    a.partial_cmp(b) == Some(std::cmp::Ordering::Less)
}

// Rule of thumb:
// - Few types, performance-critical → generics (static dispatch)
// - Many/unknown types, flexibility needed → dyn (dynamic dispatch)
// - Heterogeneous collections → must use dyn

Типаж Any и даункастинг

Типаж Any позволяет хранить значения любого типа и восстанавливать конкретный тип в runtime через даункастинг. downcast_ref::<T>() возвращает Option<&T>, downcast::<T>() возвращает Result<Box<T>, Box<dyn Any>>. Это escape-hatch Rust для случаев, когда тип действительно неизвестен при компиляции (системы плагинов, динамические конфигурации). Однако предпочитайте enum'ы, когда набор возможных типов известен — они безопаснее, быстрее и идиоматичнее. Any полагается на TypeId, реализованный для всех 'static типов.

rust
use std::any::Any;

// Any enables runtime type checking & downcasting
let x: Box<dyn Any> = Box::new(42i32);

// downcast_ref returns Option<&T>
if let Some(n) = x.downcast_ref::<i32>() {
    println!("It's an i32: {}", n);
}

// downcast returns Option<T> (for Box)
let boxed: Box<dyn Any> = Box::new("hello");
if let Ok(s) = boxed.downcast::<&str>() {
    println!("Recovered: {}", s);
}

// Useful for: plugin systems, error types, heterogeneous storage
// Avoid overusing — prefer enums when the type set is known

Методы типажей по умолчанию и supertrait'ы

Типажи могут предоставлять реализации методов по умолчанию, которые имплементёры могут переопределять или использовать как есть. Supertrait'ы (trait Named: Shape) требуют, чтобы имплементирующий тип также реализовал supertrait — это создаёт иерархию, где Named-типы гарантированно имеют area() и describe(). Методы по умолчанию уменьшают шаблонность и обеспечивают паттерн 'extension method', где добавление метода к типажу автоматически выгодно всем существующим имплементёрам без их нарушения.

rust
trait Shape {
    fn area(&self) -> f64;
    // Default method — can be overridden
    fn describe(&self) -> String {
        format!("Shape with area {:.2}", self.area())
    }
}

// Supertrait: trait that requires another trait
trait Named: Shape {
    fn name(&self) -> &str;
}

struct Circle { radius: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
}
impl Named for Circle {
    fn name(&self) -> &str { "Circle" }
}

let c = Circle { radius: 2.0 };
println!("{}", c.describe());  // uses default method
14

Макросы (декларативные и процедурные)

Декларативные макросы (macro_rules!)

macro_rules! создаёт декларативные макросы, которые разворачиваются при компиляции через сопоставление с образцом. Синтаксис $(...),* — repetition matcher — он сопоставляет ноль или более разделённых запятыми выражений. $x:expr означает 'сопоставить любое выражение и привязать его к x'. Макросы разворачиваются до проверки типов, поэтому могут генерировать код, работающий с любым типом. Используйте макросы для уменьшения шаблонности, с которой не справляются дженерики (например, вариативные аргументы, расширение синтаксиса).

rust
// macro_rules! defines pattern-matching macros
macro_rules! vec_of {
    ($($x:expr),*) => {{
        let mut v = Vec::new();
        $( v.push($x); )*
        v
    }};
}

let nums = vec_of!(1, 2, 3, 4);

// Recursive macro: build a HashMap
macro_rules! hashmap {
    ($($k:expr => $v:expr),*) => {{
        let mut m = std::collections::HashMap::new();
        $( m.insert($k, $v); )*
        m
    }};
}
let config = hashmap!("host" => "localhost", "port" => 8080);

Типы фрагментов макросов

Спецификаторы фрагментов определяют, какой синтаксис сопоставляет аргумент макроса. :ident сопоставляет идентификаторы (имена), :expr сопоставляет выражения (значения), :ty сопоставляет типы, :block сопоставляет блоки в фигурных скобках, :stmt сопоставляет инструкции, :literal сопоставляет литералы. Выбор правильного спецификатора важен — :expr наиболее распространён, но :ident нужен, когда вы хотите создать имя функции/переменной. Система макросов гигиенична: идентификаторы, введённые макросами, не конфликтуют с окружающим кодом.

rust
// Common fragment specifiers:
macro_rules! build_fn {
    // $name:ident — identifier (function/variable name)
    // $body:block — a block { ... }
    // $ty:ty — a type
    // $expr:expr — an expression
    // $stmt:stmt — a statement
    // $lit:literal — a literal (string, number)
    ($name:ident, $ret:ty, $body:block) => {
        fn $name() -> $ret $body
    };
}

build_fn!(get_answer, i32, { 42 });
println!("{}", get_answer());

// :pat — pattern, :path — module path
// :meta — attribute meta-item, :vis — visibility

Встроенные стандартные макросы

Rust поставляется со множеством встроенных макросов. println!/eprintln! печатают в stdout/stderr. dbg! печатает значение выражения с информацией о файле/строке — отлично для отладки (также возвращает значение). assert!/assert_eq!/assert_ne! — для тестов и инвариантов. todo!/unimplemented! помечают незавершённый код паникой. file!/line!/module! дают информацию о местоположении при компиляции. env!/option_env! читают переменные окружения при компиляции — полезно для встраивания информации о версии.

rust
// Formatting & printing
println!("x = {}, y = {:?}", 1, "two");
eprintln!("Error: {}", "oops");     // stderr
format!("{}-{}", "a", "b");          // returns String

// Debug helpers
dbg!(2 + 2);                          // prints [src.rs:1] 2 + 2 = 4
assert!(1 + 1 == 2);                  // panic if false
assert_eq!(2 + 2, 4);                 // panic if not equal
assert_ne!(1, 2);                     // panic if equal

// Code generation
todo!("not implemented yet");         // unimplemented!()
unimplemented!();
panic!("fatal: {}", "reason");

// Environment & file info
println!("{}:{}", file!(), line!());  // src.rs:1
println!("{}", env!("CARGO_PKG_NAME"));

Обзор процедурных макросов

Процедурные макросы (proc macros) — функции Rust, принимающие TokenStreams на вход и производящие TokenStreams на выходе — полное преобразование кода в код. В отличие от декларативных макросов, могут выполнять произвольные вычисления. Три типа: derive-макросы (добавляют реализации типажей через #[derive]), attribute-макросы (аннотируют элементы) и function-like макросы (кастомный синтаксис вроде sqlx::query!). Они должны жить в отдельном крейте с proc-macro = true. Крейт syn парсит синтаксис Rust, quote! генерирует код. Популярные примеры: serde, tokio, thiserror.

rust
// Procedural macros are Rust functions that transform code
// Three kinds (must be in a separate crate with proc-macro=true):

// 1. Derive macros — #[derive(MyTrait)]
#[derive(Debug, Clone, MyTrait)]
struct Point { x: f64, y: f64 }

// 2. Attribute macros — #[my_attr]
#[my_attr]
fn function() {}

// 3. Function-like macros — my_macro!(...)
sqlx::query!("SELECT * FROM users");

// Cargo.toml for a proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0"   # parse Rust code
// quote = "1.0" # generate code
// proc-macro2 = "1.0"

Гигиена макросов и распространённые паттерны

Макросы Rust гигиеничны — идентификаторы, созданные внутри макроса, существуют в отдельном 'контексте синтаксиса' и не будут случайно захватывать или затенять переменные в вызывающей области. Это предотвращает тонкие баги, где внутреннее имя переменной макроса конфликтует с именем вызывающего. stringify! преобразует любой токенный поток в строковый литерал при компиляции (полезно для сообщений об ошибках). cfg_debug! показывает распространённый паттерн: условная компиляция кода на основе конфигурации сборки через систему cfg!.

rust
// Hygiene: macro-introduced identifiers don't leak
macro_rules! using_temp {
    ($e:expr) => {
        let temp = $e;  // this 'temp' is distinct from caller's
        println!("{}", temp);
    };
}
let temp = 10;
using_temp!(temp + 5);  // no conflict — hygienic

// Conditional compilation macro
macro_rules! cfg_debug {
    ($($e:tt)*) => {
        #[cfg(debug_assertions)]
        { $($e)* }
    };
}
cfg_debug! {
    println!("Debug mode on");
}

// stringify! converts tokens to a string literal
let s = stringify!(a + b * c);  // "a + b * c"
15

Unsafe Rust

Сырые указатели

Сырые указатели (*const T, *mut T) — escape-hatch Rust от borrow checker'а. В отличие от ссылок, они могут быть null, могут алиасить (несколько указателей на одни данные) и не отслеживают времена жизни. Их создание безопасно, но разыменование требует unsafe, так как компилятор не может гарантировать действительность. Используйте сырые указатели для FFI (взаимодействие с C), реализации низкоуровневых структур данных (связные списки, векторы) и performance-critical кода, где вы вручную обеспечиваете безопасность. Всегда документируйте, почему unsafe корректен.

rust
// Raw pointers: *const T (immutable) and *mut T (mutable)
let x = 42;
let r1: *const i32 = &x;       // coerce from reference
let r2: *mut i32 = x as *mut i32;  // cast

// Can be null, can alias, no borrow checking
let null: *const i32 = std::ptr::null();

// Creating raw pointers is safe, but DEREFERENCING is unsafe
unsafe {
    println!("r1 = {}", *r1);
}

// Convert between types (transmute-like)
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("0x{:x}", *ptr); }  // little-endian int

Unsafe-блоки и функции

unsafe не отключает borrow checker — он позволяет делать пять конкретных вещей: (1) разыменовывать сырые указатели, (2) вызывать unsafe-функции, (3) реализовывать unsafe-типажи, (4) обращаться к static mut, (5) обращаться к полям union. unsafe-блоки делают unsafe-операции явными и локализованными. unsafe fn объявляет, что вызов функции требует соблюдения инвариантов, которые компилятор не может проверить. get_unchecked пропускает проверку границ для производительности — безопасно только если вы проверили индекс. Минимизируйте unsafe-поверхность и инкапсулируйте за безопасным API.

rust
// unsafe block: a localized unsafe region
let ptr: *const i32 = &42;
let val = unsafe { *ptr };

// unsafe fn: the ENTIRE function body is unsafe
unsafe fn dangerous(ptr: *const u8) -> u8 {
    *ptr
}
// Callers must use unsafe block:
let b = 5u8;
unsafe { dangerous(&b) };

// Splitting borrows safely (compiler is conservative)
let mut v = vec![1, 2, 3, 4];
let len = v.len();
unsafe {
    let first = v.get_unchecked(0);   // no bounds check
    let last = v.get_unchecked(len - 1);
    println!("{} {}", first, last);
}

FFI — вызов C-функций

FFI (Foreign Function Interface) позволяет Rust вызывать C-функции и наоборот. Блоки extern "C" объявляют внешние C-функции — их вызов unsafe, так как компилятор не может проверить их поведение. #[no_mangle] предотвращает переименование функции Rust, чтобы C мог найти её по имени. #[repr(C)] гарантирует, что расположение структуры соответствует памяти C (Rust может переупорядочивать поля по умолчанию для эффективности). Используйте FFI для системных вызовов, устаревших библиотек и performance-critical привязок. Крейт bindgen авто-генерирует FFI-объявления из C-заголовков.

rust
// extern "C" declares foreign functions
extern "C" {
    fn abs(x: i32) -> i32;
}

fn main() {
    let x = -5;
    let positive = unsafe { abs(x) };
    println!("{}", positive);  // 5
}

// Exporting Rust functions to C
#[no_mangle]  // prevent name mangling
pub extern "C" fn add(a: i64, b: i64) -> i64 {
    a + b
}

// C-compatible struct
#[repr(C)]
struct Point { x: f64, y: f64 }

Реализация unsafe-типажей

Unsafe-типажи (как Send, Sync) требуют, чтобы имплементёр соблюдал инварианты, которые компилятор не может проверить. Send означает, что тип может безопасно перемещаться между потоками; Sync означает, что &T может разделяться между потоками. Компилятор авто-выводит их для большинства типов, но сырые указатели не Send/Sync по умолчанию. При ручной реализации вы берёте ответственность за потокобезопасность. static mut требует unsafe-доступа, так как несколько потоков могут гонять за него — предпочитайте атомики (AtomicU64) или Mutex. Всегда документируйте обоснование безопасности комментарием SAFETY.

rust
// Some traits are unsafe to implement — the compiler can't verify invariants
use std::marker::Send;

// Send/Sync are auto-implemented, but sometimes you must manually impl
struct RawPointer<T>(*mut T);

// SAFETY: We guarantee the pointer is only used on one thread
unsafe impl<T> Send for RawPointer<T> where T: Send {}

// Splitting a slice into disjoint mutable parts
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left = [1,2,3], right = [4,5,6] — disjoint, but compiler
// couldn't prove this without unsafe internally

// Global mutable state
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; }  // unsafe: data races possible

Union'ы и inline-ассемблер

Union'ы позволяют разным типам разделять одну ячейку памяти — чтение поля, в которое не записывали последним, — undefined behavior, поэтому unsafe. Они в основном для FFI с C. transmute реинтерпретирует битовое представление одного типа в другой того же размера — крайне опасно, если размеры отличаются или типы несовместимы. Inline-ассемблер (asm!) позволяет встраивать CPU-инструкции напрямую, полезно для разработки ядра и экстремальной оптимизации. Все это острые инструменты: используйте только когда нет безопасной альтернативы, и инкапсулируйте за безопасной абстракцией.

rust
// Unions: multiple fields share the same memory (like C unions)
#[repr(C)]
union IntOrFloat {
    i: i32,
    f: f32,
}

let mut u = IntOrFloat { i: 42 };
unsafe { println!("as int: {}", u.i); }
u.f = 3.14;
unsafe { println!("as float: {}", u.f); }
// Reading the WRONG field is undefined behavior!

// Inline assembly (nightly / asm!)
#[cfg(feature = "asm")]
unsafe fn halt() {
    std::arch::asm!("hlt", options(nostack));
}

// Transmute: reinterpret bits as another type (same size)
let bits: u32 = 0x40490FDB;  // ~3.14159 in IEEE 754
let pi: f32 = unsafe { std::mem::transmute(bits) };
16

Углублённое изучение итераторов

Типаж Iterator и создание

Типаж Iterator требует только метод next(), возвращающий Option<Item> — None сигнализирует об исчерпании. Всё остальное (map, filter, collect) строится сверху. iter() заимствует элементы (&T), into_iter() потребляет коллекцию (возвращает owned T), iter_mut() возвращает &mut T. Диапазоны (1..5, 1..=5) — итераторы напрямую. Строки итерируют по символам (Unicode scalar values) или байтам. Итераторы ленивы — ничего не выполняется, пока вы их не потребите.

rust
// The Iterator trait: one required method
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... many provided methods (map, filter, etc.)
}

// Creating iterators
let v = vec![1, 2, 3];
let iter = v.iter();        // borrows: &i32
let into_iter = v.into_iter(); // owns: i32 (consumes v)
let mut_iter = v.iter_mut();   // mutably borrows: &mut i32

// Range is an iterator
for i in 1..=5 { print!("{} ", i); }  // 1 2 3 4 5

// String iteration
for c in "héllo".chars() { print!("{} ", c); }  // h é l l o
for b in "hi".bytes() { print!("{} ", b); }      // 104 105

Методы-адаптеры (ленивые)

Методы-адаптеры преобразуют итераторы и возвращают новые итераторы — они ленивые, поэтому цепочка map().filter().map() создаёт ноль промежуточных коллекций. map применяет функцию к каждому элементу. filter оставляет элементы, где предикат возвращает true. take(n) останавливает после n элементов (полезно для бесконечных итераторов). skip(n) отбрасывает первые n. flat_map отображает и выравнивает вложенные итераторы. enumerate соединяет каждый элемент с индексом. Ничего не выполняется, пока потребитель (collect, sum, for-цикл) не запустит итератор.

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

// map: transform each element
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();

// filter: keep elements matching predicate
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// take / skip: limit or skip elements
let first3: Vec<_> = nums.iter().take(3).collect();     // [1,2,3]
let after2: Vec<_> = nums.iter().skip(2).collect();     // [3,4,5,6]

// flat_map: map then flatten one level
let words: Vec<_> = ["a b", "c"].iter()
    .flat_map(|s| s.split(' '))
    .collect();  // ["a","b","c"]

// enumerate: add index
for (i, v) in nums.iter().enumerate() {
    println!("{}: {}", i, v);
}

Методы-потребители

Методы-потребители запускают ленивую цепочку итераторов на выполнение. collect() собирает результаты в любую коллекцию, реализующую FromIterator (Vec, HashMap, String и т.д.). sum/product/count/fold редуцируют итератор до одного значения. find/any/all с коротким замыканием — они останавливаются, как только ответ известен, поэтому эффективны на бесконечных итераторах. min/max возвращают Option (None для пустых итераторов). Комбинация ленивых адаптеров + финального потребителя означает, что цепочки итераторов столь же эффективны, как рукописные циклы после оптимизации.

rust
let nums = vec![1, 2, 3, 4, 5];

// collect: gather into a collection
let v: Vec<i32> = nums.iter().copied().collect();
let s: std::collections::HashSet<i32> = nums.iter().copied().collect();

// sum, product, count
let total: i32 = nums.iter().sum();        // 15
let prod: i32 = nums.iter().product();     // 120
let count = nums.iter().count();           // 5

// reduce / fold: accumulate into a single value
let max = nums.iter().copied().reduce(i32::max);  // Some(5)
let sum = nums.iter().fold(0, |acc, &x| acc + x); // 15

// find / any / all: short-circuiting
let first_even = nums.iter().find(|&&x| x % 2 == 0);  // Some(2)
let has_neg = nums.iter().any(|&x| *x < 0);  // false
let all_pos = nums.iter().all(|&x| *x > 0);  // true

// min / max
nums.iter().copied().min();  // Some(1)
nums.iter().copied().max();  // Some(5)

Пользовательские итераторы

Для создания пользовательского итератора реализуйте типаж Iterator с методом next(). После этого вы получаете все 70+ методов-адаптеров и потребителей бесплатно. Итератор должен отслеживать своё состояние (текущая позиция и т.д.) и возвращать None при исчерпании. Реализация IntoIterator для вашего типа коллекции включает синтаксис for-цикла. Для двунаправленного или случайного доступа также реализуйте DoubleEndedIterator или ExactSizeIterator. Так Vec, HashMap, Range и все стандартные коллекции обеспечивают итерацию.

rust
// Build a custom iterator for a counter
struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Self {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let val = self.current;
            self.current += 1;
            Some(val)
        } else {
            None
        }
    }
}

// Now all iterator methods work!
let sum: usize = Counter::new(5).sum();  // 0+1+2+3+4 = 10
let doubled: Vec<_> = Counter::new(3).map(|x| x * 10).collect();

Бесконечные и цепочечные итераторы

Итераторы Rust могут быть бесконечными — (1..) генерирует натуральные числа вечно, repeat(x) повторяет бесконечно. Они безопасны, так как адаптеры ленивые: take(n) ограничивает потребление. cycle() повторяет конечный итератор бесконечно. chain() конкатенирует итераторы последовательно. zip() соединяет элементы позиционно (останавливается на более коротком). peekable() позволяет посмотреть следующий элемент без потребления — полезно для парсеров. Ленивый дизайн означает, что бесконечные итераторы ничего не стоят, пока не потребляются, а компилятор оптимизирует цепочки в плотные циклы.

rust
use std::iter;

// Infinite iterators (use with take!)
let naturals = (1..).take(10);  // 1..10
let zeros = iter::repeat(0).take(5);  // [0,0,0,0,0]
let alternating = iter::repeat_with(|| rand::random::<u8>());

// cycle: repeat a finite iterator infinitely
let pattern = [1, 2, 3].iter().cycle().take(7);
// [1,2,3,1,2,3,1]

// chain: concatenate two iterators
let combined = [1, 2].iter().chain([3, 4].iter());
// [1,2,3,4]

// zip: pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter().zip(['a', 'b', 'c']).collect();
// [(1,'a'), (2,'b'), (3,'c')]

// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
if let Some(&&first) = iter.peek() { println!("{}", first); }
17

Углублённое изучение обработки ошибок

Комбинаторы Result и Option

Комбинаторы позволяют цепочечно выполнять fallible-операции без вложенных match. map преобразует Ok-значение, map_err преобразует ошибку. and_then цепочит операции, сами возвращающие Result (flatmap для ошибок). ok_or преобразует Option→Result. unwrap_or/unwrap_or_else/unwrap_or_default предоставляют fallback-значения. Они компонуются элегантно: parse().map().and_then().map_err() создаёт конвейер, где каждый шаг может завершиться неудачей, а первая неудача коротко замыкает. Предпочитайте комбинаторы unwrap() в production-коде.

rust
// Result combinators chain operations without match
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>().map(|n| n * 2)
}

// and_then: chain fallible operations
fn validate(n: i32) -> Result<i32, String> {
    if n > 0 { Ok(n) } else { Err("must be positive".into()) }
}
let result = "5".parse::<i32>().and_then(validate);  // Ok(5)

// map_err: transform the error type
let r = "abc".parse::<i32>()
    .map_err(|e| format!("Parse failed: {}", e));

// ok_or / ok_or_else: convert Option to Result
let opt: Option<i32> = None;
let val = opt.ok_or("missing value");  // Err("missing value")

// unwrap_or / unwrap_or_else / unwrap_or_default
let x: i32 = "abc".parse().unwrap_or(0);

Пользовательские типы ошибок

Пользовательские типы ошибок позволяют представлять domain-specific неудачи. Ключевой паттерн: реализуйте From для каждого базового типа ошибки, чтобы оператор ? авто-конвертировал. Это значит, что можно использовать ? с std::io::Error, ParseIntError и т.д. без явного map_err. Реализация Display делает ошибку дружелюбной; Debug — для разработчиков. Enum-основанные ошибки идиоматичны в Rust — они исчерпывающие (компилятор предупреждает о пропущенных случаях) и zero-cost (без выделения кучи). Это основа перед использованием thiserror.

rust
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
    Unauthorized,
}

// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(s) => write!(f, "Not found: {}", s),
            AppError::Unauthorized => write!(f, "Unauthorized"),
        }
    }
}

// Now ? works for io::Error and ParseIntError automatically
fn read_config() -> Result<i32, AppError> {
    let s = std::fs::read_to_string("config.txt")?;  // io → AppError
    let n: i32 = s.trim().parse()?;                   // parse → AppError
    Ok(n)
}

Типаж Error и Box<dyn Error>

std::error::Error — стандартный библиотечный типаж для типов ошибок (требует Debug + Display). Box<dyn Error> — простейший тип ошибки — принимает любую ошибку через ? и отлично подходит для прототипирования или приложений, где не нужно программно обрабатывать конкретные ошибки. Недостаток: теряется конкретный тип ошибки, поэтому сопоставление с конкретными вариантами требует downcast_ref. Для библиотек предпочитайте конкретный enum-тип ошибки (с thiserror). Для приложений anyhow лучше Box<dyn Error>, так как сохраняет backtrace'ы и цепочки ошибок.

rust
use std::error::Error;

// std::error::Error is the trait for all errors
// Requires: Debug + Display
fn do_something() -> Result<(), Box<dyn Error>> {
    let f = std::fs::read_to_string("file.txt")?;  // io::Error
    let n: i32 = f.parse()?;                         // ParseIntError
    println!("{}", n);
    Ok(())
}

// Box<dyn Error> is the quick-and-dirty error type
// — accepts any error via ?, but loses specific type info

// downcast to recover specific error type
match do_something() {
    Err(e) => {
        if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
            println!("IO: {}", io_err);
        }
    }
    _ => {}
}

Крейт thiserror (ошибки библиотек)

thiserror — стандартный крейт для типов ошибок библиотек. Макрос #[derive(Error)] генерирует Display (из #[error("...")]) и From (из #[from]) автоматически. #[from] заставляет ? конвертировать базовую ошибку в ваш enum-вариант. Это устраняет шаблонность, сохраняя строго типизированный, исчерпывающий enum ошибок. Используйте thiserror для библиотек (где вызывающим нужно сопоставлять конкретные ошибки). Плейсхолдер {0} вставляет Display внутренней ошибки; именованные поля вроде {id} вставляют поля структуры.

rust
use thiserror::Error;

// thiserror auto-generates Display and From impls
#[derive(Debug, Error)]
enum DataError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("parse failed: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("item {id} not found")]
    NotFound { id: u32 },

    #[error("invalid state: {msg}")]
    Invalid { msg: String },
}

// #[from] auto-implements From, so ? just works:
fn load() -> Result<i32, DataError> {
    let s = std::fs::read_to_string("data.txt")?;  // auto-converts
    let n: i32 = s.trim().parse()?;
    Ok(n)
}

Крейт anyhow (ошибки приложений)

anyhow — стандартный крейт для обработки ошибок приложений/бинарников. anyhow::Error оборачивает любую ошибку, реализующую std::error::Error, и добавляет контекст, backtrace'ы и цепочки ошибок. context() прикрепляет человекочитаемое сообщение к каждому fallible-шагу, создавая цепочку вроде 'Failed to read config: IO error: No such file'. Это значительно облегчает отладку — вы видите, какой шаг завершился неудачей и почему. Используйте anyhow для main() и кода приложения, где нужно только сообщать об ошибках, а не сопоставлять их. Используйте thiserror для библиотек, где вызывающим нужны типизированные ошибки.

rust
use anyhow::{Context, Result, anyhow};

// anyhow::Result = Result<T, anyhow::Error>
fn load_config() -> Result<String> {
    let content = std::fs::read_to_string("config.toml")
        .context("Failed to read config.toml")?;  // add context
    Ok(content)
}

fn main() -> Result<()> {
    let config = load_config()?;
    if config.is_empty() {
        // bail! / anyhow! create errors with format! syntax
        return Err(anyhow!("config is empty"));
    }
    println!("{}", config);
    Ok(())
}

// Error chain: "Failed to read config.toml: No such file..."
// anyhow preserves the full chain and backtrace
18

Углублённое изучение Cargo и крейтов

Структура Cargo.toml

Cargo.toml — манифест проекта Rust. [package] описывает метаданные крейта. [dependencies] перечисляет внешние крейты — строки версий используют semver (^1.0 означает >=1.0, <2.0). features включают опциональную функциональность (derive-функция serde включает #[derive(Serialize)]). [dev-dependencies] — только для тестов/бенчмарков. [features] определяет флаги условной компиляции. [profile.release] управляет настройками оптимизации. Поле edition (2015/2018/2021) определяет языковые функции — всегда используйте последнюю.

rust
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
description = "A sample app"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
rand = "0.8"

[dev-dependencies]
criterion = "0.5"  # only for tests/benches

[features]
default = ["tokio"]
async-mode = ["dep:tokio"]

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

[profile.release]
opt-level = 3
lto = true
strip = true

Зависимости и флаги features

Флаги features включают условную компиляцию. Каждая зависимость может открывать features (например, derive у serde). default-features = false убирает features по умолчанию для уменьшения размера бинарника. Опциональные зависимости (optional = true) компилируются только когда feature включает их через dep:name. Features аддитивны — они включают, никогда не выключают. Это обеспечивает feature unification: если две зависимости включают разные features serde, Cargo компилирует serde один раз с объединением всех features. Используйте #[cfg(feature = "x")] для условной компиляции кода.

rust
# Cargo.toml
[dependencies]
# Version requirements: ^1.2 (compatible), =1.2.3 (exact), >=1.0,<2.0 (range)
serde = "1.0"                    # ^1.0 (default: compatible)
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false }  # disable defaults

# Optional dependencies (enabled by a feature)
tokio = { version = "1", optional = true }

[features]
# "async" feature enables the optional tokio dep
async = ["dep:tokio"]
# Features can enable other features
full = ["async", "serde/derive"]

# In code:
# #[cfg(feature = "async")]
# fn run_async() { ... }

Workspace'ы (мульти-крейтовые проекты)

Workspace'ы группируют несколько связанных крейтов, разделяющих Cargo.lock и целевую директорию. Это ускоряет сборку (общий кэш компиляции) и гарантирует, что все крейты используют одинаковые версии зависимостей. Члены могут зависеть друг от друга через path = "../core". [workspace.dependencies] централизует управление версиями — крейты-члены ссылаются на них через { workspace = true }. resolver = "2" (по умолчанию в edition 2021) использует feature unification по-цели, избегая некоторых проблем сборки. Используйте workspace'ы для monorepos, библиотек с несколькими компонентами или проектов, разделяющих core/CLI/server.

rust
# Root Cargo.toml — workspace manifest
[workspace]
members = [
    "core",
    "cli",
    "server",
    "utils",
]
resolver = "2"

# Shared dependencies across all members
[workspace.dependencies]
serde = "1.0"
tokio = "1"

# In member crates (e.g., cli/Cargo.toml):
# [dependencies]
# serde = { workspace = true }
# core = { path = "../core" }  # local path dependency

Профили сборки и оптимизация

Профили управляют тем, как cargo собирает проект. dev (по умолчанию для cargo build) приоритизирует скорость компиляции. release (cargo build --release) приоритизирует runtime-производительность. Ключевые параметры: opt-level (0-3, 's' для размера, 'z' для мин. размера), lto (link-time optimization через границы крейтов), codegen-units (1 = лучшая оптимизация, но самая медленная компиляция), strip (удаление символов для меньших бинарников), panic = 'abort' (отключает unwinding, меньший бинарник). Для production используйте lto = true, codegen-units = 1, strip = true. Кастомные профили наследуются от существующих.

rust
# Cargo.toml
[profile.dev]
opt-level = 0        # no optimization (fast compile)
debug = true         # include debug symbols
overflow-checks = true

[profile.release]
opt-level = 3        # max optimization
lto = "fat"          # link-time optimization across crates
codegen-units = 1    # single codegen unit (better opt, slower compile)
strip = true         # strip debug symbols from binary
panic = "abort"      # smaller binary, no unwinding

[profile.release.package."*"]
opt-level = 2  # optimize dependencies less than your code

# Custom profile
[profile.bench]
inherits = "release"
debug = true  # keep symbols for profiling

# Usage: cargo build --release --profile bench

Публикация и документация

Публикация на crates.io перманентна — версии нельзя перезаписать или удалить (только yank, что предотвращает новых зависимых). Убедитесь, что name, version, description, license и repository установлены. cargo package валидирует манифест и показывает, что будет опубликовано. cargo doc генерирует HTML-документацию из /// doc-комментариев — doc-тесты (код в блоках ```) компилируются и запускаются cargo test. Хорошие doc-комментарии с примерами — это и документация, и тесты. Используйте #[doc(hidden)] для сокрытия внутренних элементов.

rust
# Before publishing:
# 1. Login (one-time)
# $ cargo login <token>  (token from crates.io)

# 2. Check the package
# $ cargo package        # creates .crate file, checks metadata
# $ cargo publish --dry-run

# 3. Publish
# $ cargo publish        # uploads to crates.io (irreversible!)

# Required fields in Cargo.toml for publishing:
# name, version, description, license, repository

# Documentation:
# $ cargo doc            # generate docs for your crate + deps
# $ cargo doc --open     # generate and open in browser
# $ cargo doc --no-deps  # only your crate

# Doc tests run automatically:
/// Adds two numbers.
/// 
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
19

Trait-объекты и динамическая диспетчеризация

Основы dyn Trait

Trait-объекты (dyn Trait) обеспечивают runtime-полиморфизм: одна переменная может хранить разные конкретные типы, реализующие один типаж. Компилятор генерирует vtable (виртуальную таблицу методов) для каждого типа, а вызовы методов проходят через vtable (динамическая диспетчеризация). Это имеет небольшую runtime-стоимость, но обеспечивает гетерогенные коллекции. Используйте trait-объекты, когда конкретный тип неизвестен при компиляции или варьируется.

rust
trait Draw {
    fn draw(&self);
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Draw for Circle {
    fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Square {
    fn draw(&self) { println!("Square s={}", self.side); }
}

// Trait object: type-erased, dynamic dispatch
let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }

// Function taking trait object
fn render(shape: &dyn Draw) { shape.draw(); }

Объектная безопасность

Типаж объектно-безопасен (может использоваться как dyn Trait), только если: нет методов, возвращающих Self, нет методов, принимающих Self по значению, нет дженерик-методов, и все методы диспетчеризируемы. Clone, Default и From НЕ объектно-безопасны. Workaround'ы включают возврат Box<Self> (паттерн box_clone), разделение типажей или использование статической диспетчеризации с enum'ами. Компилятор ясно сообщает о нарушениях объектной безопасности.

rust
// Object-safe trait (can be made into dyn)
trait Animal {
    fn sound(&self) -> String;
    fn name(&self) -> &str;
}

// NOT object-safe: returns Self
trait Clone {
    fn clone(&self) -> Self;  // Self is unknown for dyn
}

// NOT object-safe: takes Self by value
trait Add {
    fn add(&self, other: Self) -> Self;
}

// NOT object-safe: generic method
trait From {
    fn from<T>(t: T) -> Self;
}

// Fix: use where clauses or separate traits
trait AnimalSafe {
    fn sound(&self) -> String;
    fn box_clone(&self) -> Box<dyn AnimalSafe>;
}

Статическая vs динамическая диспетчеризация

Статическая диспетчеризация (дженерики с границами типажей) мономорфизирует: компилятор генерирует специализированную версию для каждого конкретного типа, обеспечивая inlining и максимальную производительность ценой размера бинарника. Динамическая диспетчеризация (dyn Trait) использует vtable-lookup в runtime — меньший бинарник, но более медленные вызовы (предотвращает inlining). Предпочитайте статическую диспетчеризацию для performance-critical кода; используйте динамическую для гетерогенных коллекций и систем плагинов.

rust
// Static dispatch (monomorphization)
fn max<T: Ord>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
// Compiler generates max_i32, max_f64, etc.

// Dynamic dispatch (vtable lookup)
fn max_dyn(a: &dyn Ord, b: &dyn Ord) -> bool {
    // a > b  // Cannot use operators on dyn
    false
}

// Trait bound (static)
fn process<T: Display>(item: &T) {
    println!("{}", item);
}

// impl Trait (static, syntactic sugar)
fn process2(item: &impl Display) {
    println!("{}", item);
}

// dyn Trait (dynamic)
fn process3(item: &dyn Display) {
    println!("{}", item);
}

Trait-объекты с временами жизни

Trait-объекты могут нести границы времён жизни: Box<dyn Trait + 'a> означает, что trait-объект (и конкретный тип за ним) должен жить как минимум 'a. По умолчанию Box<dyn Trait> подразумевает 'static. При хранении trait-объектов, которые могут содержать ссылки, добавьте время жизни явно. Синтаксис + комбинирует границы типажей с временами жизни. Это распространено в системах плагинов и обработчиках событий.

rust
trait Parser {
    fn parse(&self, input: &str) -> &str;
}

// Trait object with lifetime
fn make_parser() -> Box<dyn Parser> {
    Box::new(MyParser)
}

// Trait object holding references
struct Runner<'a> {
    parsers: Vec<Box<dyn Parser + 'a>>,
}

impl<'a> Runner<'a> {
    fn add(&mut self, p: Box<dyn Parser + 'a>) {
        self.parsers.push(p);
    }
}

// dyn Trait defaults to 'static when no lifetime given
fn static_parser() -> Box<dyn Parser> {
    Box::new(MyParser)
}

Даункастинг и Any

Типаж Any включает runtime-проверку типов и даункастинг. Any автоматически реализуется для всех 'static типов. downcast_ref и downcast_mut возвращают Option, позволяя безопасное восстановление типа. Это полезно для систем плагинов, динамических конфигураций и гетерогенных контейнеров. Используйте Any экономно — он обход систему типов. Предпочитайте enum'ы для известных альтернатив и дженерики для типобезопасного полиморфизма.

rust
use std::any::Any;

// Any enables runtime type identification
let x: Box<dyn Any> = Box::new(42_i32);

// Downcast to concrete type
if let Some(n) = x.downcast_ref::<i32>() {
    println!("Got i32: {}", n);
}

// Store heterogeneous values
let mut bag: Vec<Box<dyn Any>> = vec![
    Box::new(42_i32),
    Box::new("hello".to_string()),
    Box::new(3.14_f64),
];

for item in &bag {
    if let Some(s) = item.downcast_ref::<String>() {
        println!("String: {}", s);
    } else if let Some(n) = item.downcast_ref::<i32>() {
        println!("i32: {}", n);
    }
}
20

Декларативные макросы

Основы macro_rules!

macro_rules! определяет декларативные макросы, которые сопоставляют паттерны и разворачиваются в код. $( $x:expr ),* сопоставляет разделённый запятыми список выражений, повторяющийся ноль или более раз. Блок $() ... * повторяется для каждого совпадения. Макросы разворачиваются при компиляции до проверки типов. Они гигиеничны: идентификаторы, введённые макросом, не конфликтуют с окружающим кодом. Используйте макросы для уменьшения шаблонности (vec!, println!, format!).

rust
macro_rules! vec_of {
    ( $( $x:expr ),* ) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

let nums = vec_of!(1, 2, 3, 4);
let strs = vec_of!("a", "b", "c");

// Multiple patterns
macro_rules! greet {
    () => { println!("Hello!") };
    ($name:expr) => { println!("Hello, {}!", $name) };
    ($name:expr, $greeting:expr) => {
        println!("{}, {}!", $greeting, $name)
    };
}
greet!();
greet!("Alice");
greet!("Bob", "Hi");

Типы фрагментов

Фрагменты макросов имеют специфические типы: expr (выражения), stmt (инструкции), ty (типы), pat (паттерны), ident (идентификаторы), tt (token trees, самый гибкий), literal (литералы) и др. Тип фрагмента определяет, что макрос принимает и как парсит. tt — самый общий — любая валидная последовательность токенов. Используйте наиболее специфичный тип для лучших сообщений об ошибках. Парсер следует правилу Most-Recently-Added-Ambiguity.

rust
macro_rules! items {
    // $x:expr - expression (1 + 2, foo())
    // $x:stmt - statement (let x = 5;)
    // $x:ty - type (Vec<i32>, &str)
    // $x:pat - pattern (Some(x), (a, b))
    // $x:path - path (std::vec::Vec, Module::Type)
    // $x:ident - identifier (foo, Bar)
    // $x:literal - literal (42, "hello")
    // $x:tt - token tree (anything)
    // $x:block - block ({ ... })
    // $x:item - item (fn, struct)

    ($e:expr, $t:ty, $i:ident) => {
        let $i: $t = $e;
    };
}

items!(42, i32, my_num);
items!("hi", &str, greeting);

Паттерны повторения

Повторение в макросах: $(...)* сопоставляет ноль или более, $(...)+ один или более, $(...)? ноль или один. Разделители вроде запятых идут между совпадениями. Вложенные повторения обрабатывают многомерные данные (матрицы, списки списков). Блок разворачивания $() ... * повторяется для каждого совпадения. Несколько переменных в одном повторении должны совпадать одинаковое число раз. Используйте внутренние правила @prefix для паттернов аккумулятора.

rust
macro_rules! sum {
    // Zero or more: $(...)*
    ( $( $x:expr ),* ) => {
        {
            let mut total = 0;
            $(
                total += $x;
            )*
            total
        }
    };

    // One or more: $(...)+
    ( first $(, $x:expr )+ ) => {
        println!("First and more");
    };

    // With separator and count
    ( $( $x:expr ),+ $(; $sep:expr )? ) => {
        println!("List with optional separator");
    };
}

// Nested repetition
macro_rules! matrix {
    ( $( [ $( $x:expr ),* ] ),* ) => {
        vec![ vec![ $( $x ),* ],* ]
    };
}

Гигиена и экспорт

Гигиена макросов предотвращает конфликты идентификаторов: переменные, введённые макросом, живут в своей области и не захватывают и не затеняют переменные вызывающего. Используйте $crate для ссылки на элементы в крейте, где макрос определён, обеспечивая работу после реэкспорта. Конвенция @prefix отмечает внутренние helper-правила, которые пользователи не должны вызывать напрямую. #[macro_export] публикует макрос в корне крейта.

rust
// Export at crate root
#[macro_export]
macro_rules! log {
    ($($arg:tt)*) => {
        // Hygienic: this T does not collide with caller's T
        let _ts: std::time::Instant = std::time::Instant::now();
        eprintln!("[{}] {}", "LOG", format!($($arg)*));
    };
}

// Use $crate to refer to crate items (works after re-export)
#[macro_export]
macro_rules! make_error {
    ($msg:expr) => {
        $crate::Error::new($msg)
    };
}

// Internal helper rule (convention: @ prefix)
macro_rules! count {
    (@count $x:expr, $($rest:expr),*) => { 1 + count!(@count $($rest),*) };
    (@count $x:expr) => { 1 };
    () => { 0 };
}

Распространённые паттерны макросов

Макросы превосходны в построении DSL и уменьшении шаблонности. Распространённые паттерны: builder DSL (html!, sql!), test assertions (assert_approx!), конфигурация (config!) и генерация кода (derive-подобные макросы). Макросы гигиеничны и compile-time, поэтому не имеют runtime-стоимости. Ограничения: нет рекурсии глубже 64, сложные сообщения об ошибках и трудности с нетривиальным парсингом. Для сложного метапрограммирования используйте процедурные макросы.

rust
// 1. Builder DSL
macro_rules! html {
    ($tag:ident { $($body:tt)* }) => {
        format!("<{}>{}</{}>", stringify!($tag), html_inner!($($body)*), stringify!($tag))
    };
}

// 2. Test assertion
macro_rules! assert_approx {
    ($a:expr, $b:expr, $eps:expr) => {
        assert!(($a - $b).abs() < $eps, "{} != {} within {}", $a, $b, $eps);
    };
}

// 3. Configuration
macro_rules! config {
    ( $( $key:ident : $val:expr ),* ) => {
        {
            let mut m = std::collections::HashMap::new();
            $(
                m.insert(stringify!($key).to_string(), $val.to_string());
            )*
            m
        }
    };
}

let cfg = config! { host: "localhost", port: 8080 };
21

Cargo и workspace'ы

Настройка workspace

Workspace'ы группируют несколько крейтов, разделяющих зависимости и целевую директорию. Члены перечислены явно или через glob'ы. [workspace.package] определяет общие метаданные пакета, наследуемые через .workspace = true. [workspace.dependencies] централизует версии зависимостей, гарантируя, что все крейты используют одну версию. Это предотвращает конфликты версий и ускоряет сборку (один Cargo.lock). Используйте workspace'ы для мульти-крейтовых проектов вроде CLI + библиотека + сервер.

rust
# Root Cargo.toml
[workspace]
members = ["crates/*", "cli", "server"]
resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <[email protected]>"]
license = "MIT"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"

# Member crate: crates/mylib/Cargo.toml
[package]
name = "mylib"
version.workspace = true
edition.workspace = true

[dependencies]
serde.workspace = true
tokio.workspace = true

Профили сборки

Профили управляют настройками компиляции. dev приоритизирует быструю компиляцию (opt-level 0, debug-символы). release максимизирует runtime-производительность (opt-level 3, LTO, одна codegen-unit). LTO (Link-Time Optimization) включает cross-crate inlining. panic = "abort" производит меньшие бинарники, но отключает unwinding. Per-package overrides (profile.dev.package."*") оптимизируют зависимости даже в dev. Кастомные профили наследуются от существующих.

rust
# Cargo.toml
[profile.dev]
opt-level = 0        # No optimization (fast compile)
debug = true         # Include debug symbols
overflow-checks = true

[profile.release]
opt-level = 3        # Max optimization
debug = false
lto = "fat"          # Link-time optimization
codegen-units = 1    # Single unit (better opt, slower compile)
panic = "abort"      # Smaller binary, no unwinding
strip = true         # Strip symbols

[profile.dev.package."*"]
opt-level = 2        # Optimize dependencies in dev

# Custom profile
[profile.bench]
inherits = "release"
debug = true

# Usage: cargo build --release, cargo build --profile=bench

Features и условная компиляция

Features включают условную компиляцию. Опциональные зависимости автоматически становятся features. cfg(feature = "...") gates код по feature. Набор features по умолчанию включён, если не передан --no-default-features. Features должны быть аддитивными (включают больше, а не меньше). Используйте features для уменьшения размера бинарника, поддержки нескольких бэкендов или gates экспериментального кода. Комбинируйте с cfg_attr для условных derive-макросов. Избегайте взаимоисключающих features.

rust
# Cargo.toml
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
async-runtime = ["tokio"]

[dependencies]
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }

# Code: conditional compilation
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
    serde_json::from_str(s)
}

#[cfg(not(feature = "json"))]
pub fn parse_json(_: &str) -> Result<Value, Error> {
    Err(Error::FeatureNotEnabled)
}

Скрипты сборки (build.rs)

build.rs запускается перед компиляцией, обеспечивая генерацию кода, встраивание окружения и линковку C-библиотек. Директивы cargo:rerun-if-* управляют, когда скрипт перезапускается. Генерируйте код макросами println!, затем include! его в крейт. Распространённые применения: встраивание информации о версии, генерация привязок (bindgen), компиляция protobuf/SQL-схем и линковка системных библиотек. Держите скрипты сборки быстрыми — они запускаются при каждой сборке.

rust
// build.rs: runs before compilation
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    // Tell Cargo to rerun if env changes
    println!("cargo:rerun-if-env-changed=DATABASE_URL");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("config.rs");

    let db_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite://default.db".to_string());

    // Generate Rust code at build time
    fs::write(&dest, format!(
        "pub const DATABASE_URL: &str = \"{}\";",
        db_url
    )).unwrap();

    // Link a C library
    println!("cargo:rustc-link-lib=static=mylib");
    println!("cargo:rustc-link-search=native=/usr/local/lib");
}

// In code: include generated file
include!(concat!(env!("OUT_DIR"), "/config.rs"));

Публикация и документация

cargo publish загружает крейт на crates.io. Используйте --dry-run для проверки перед публикацией. cargo doc генерирует HTML-документацию из doc-комментариев (///). Блоки кода в doc-комментариях тестируются cargo test --doc. Включайте секции Examples, Panics и Errors. Метаданные (description, repository, keywords) улучшают discoverability. После публикации версию нельзя переиспользовать или удалить — используйте yank для предотвращения зависимости от неё новых проектов.

rust
# Publish to crates.io
cargo login <token>     # One-time authentication
cargo publish           # Publish current crate

# Before publishing:
cargo publish --dry-run # Verify package contents
cargo package           # Inspect the .crate file

# Documentation
cargo doc               # Generate docs
cargo doc --open        # Generate and open
cargo doc --no-deps     # Only this crate

# In code: doc comments
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

# README and metadata in Cargo.toml
[package]
description = "A short description"
repository = "https://github.com/user/repo"
readme = "README.md"
keywords = ["parser", "cli"]
categories = ["command-line-utilities"]
22

Процедурные макросы

Типы макросов

Процедурные макросы генерируют Rust-код при компиляции, оперируя токенными потоками. Три типа: function-like (custom!()), derive (#[derive(Custom)]), attribute (#[custom]). Требуют отдельный крейт с proc-macro = true. Крейт syn парсит синтаксис Rust, quote генерирует код, proc-macro2 включает тестирование. Proc-макросы мощные, но сложные — используйте их для derive-макросов, DSL и генерации кода, с которыми не справляются декларативные макросы.

rust
// Three types of procedural macros:
// 1. Function-like: my_macro!(...)
// 2. Derive: #[derive(MyMacro)]
// 3. Attribute: #[my_macro]

// Cargo.toml for a proc-macro crate
// [lib]
// proc-macro = true

// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> i32 { 42 }".parse().unwrap()
}

// Usage: make_answer!();
// Generates: fn answer() -> i32 { 42 }

Derive-макрос

Derive-макросы добавляют реализации типажей к типам, аннотированным #[derive(MyMacro)]. syn парсит вход в AST DeriveInput. quote! генерирует код с # интерполяцией для переменных. Helper-атрибуты (attributes(hello)) позволяют кастомизацию на полях или вариантах. Распространённые derive-макросы: Debug, Clone, Serialize, Deserialize. Сгенерированный код добавляется к модулю, поэтому не может изменять исходный тип.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;

    let expanded = quote! {
        impl HelloMacro for #name {
            fn hello() {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };

    expanded.into()
}

// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
// Pancakes::hello();  // "Hello from Pancakes!"

// Helper attributes
#[proc_macro_derive(HelloMacro, attributes(hello))]
pub fn hello_with_attr(input: TokenStream) -> TokenStream { /* ... */ }

Attribute-макрос

Attribute-макросы (#[my_attr]) преобразуют аннотируемый элемент, возможно заменяя его целиком. Получают и аргументы атрибута, и аннотируемый элемент. Распространённые применения: логирование, кэширование, async-обёртки (#[tokio::main]) и маршрутизация (#[get("/path")]). Attribute-макросы могут менять сигнатуру элемента, добавлять код или генерировать дополнительные элементы. Они более гибкие, чем derive-макросы, но сложнее в правильном использовании.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
    let attr_args = syn::parse_macro_input!(attr as syn::AttributeArgs);
    let input_fn = parse_macro_input!(item as ItemFn);

    let fn_name = &input_fn.sig.ident;
    let fn_block = &input_fn.block;

    let expanded = quote! {
        fn #fn_name() {
            println!("Calling {}", stringify!(#fn_name));
            let __result = (|| #fn_block)();
            println!("Finished {}", stringify!(#fn_name));
            __result
        }
    };

    expanded.into()
}

// Usage:
// #[log_calls]
// fn my_function() -> i32 { 42 }

Function-like макрос

Function-like процедурные макросы (my_macro!()) принимают произвольные токенные потоки, обеспечивая кастомные DSL. Реализуйте Parse для определения принимаемого синтаксиса. Макрос может валидировать, преобразовывать или генерировать код на основе входа. Распространённые применения: SQL-запросы (sqlx), HTML-шаблоны (maud) и конфигурационные DSL. В отличие от декларативных макросов, proc-макросы могут парсить сложный синтаксис и выполнять произвольные вычисления при компиляции. Держите их быстрыми, чтобы избежать медленных сборок.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input};

// Custom syntax: sql!(SELECT * FROM users WHERE id = $1)
struct SqlQuery {
    query: String,
}

impl Parse for SqlQuery {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let query = input.to_string();
        Ok(SqlQuery { query })
    }
}

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let SqlQuery { query } = parse_macro_input!(input as SqlQuery);

    let expanded = quote! {
        {
            static QUERY: &str = #query;
            // Compile-time SQL validation could go here
            QUERY
        }
    };

    expanded.into()
}

Тестирование и отладка

Тестирование proc-макросов: trybuild запускает UI-тесты, сравнивая вывод компилятора (успех или сообщения об ошибках) с ожидаемыми файлами. Для derive-макросов проверяйте, что сгенерированный код компилируется и ведёт себя корректно. Отладка через eprintln! (печатается при компиляции) или cargo expand (показывает развёрнутый вывод макроса). Разработка макросов итеративна: пишете макрос, используете cargo expand для инспекции вывода, исправляете проблемы. Чётко документируйте синтаксис и поддерживаемые функции макроса.

rust
// Cargo.toml
// [dev-dependencies]
// trybuild = "1"

// tests/ui/my_macro.rs - test file
// #[derive(MyMacro)]
// struct Foo;
// fn main() { Foo::hello(); }

// tests/ui/my_macro.stderr - expected error
// error: ...

// Test runner
#[test]
fn ui() {
    let t = trybuild::TestCases::new();
    t.pass("tests/ui/pass_*.rs");
    t.compile_fail("tests/ui/fail_*.rs");
}

// Debugging with eprintln
#[proc_macro_derive(Debug)]
pub fn debug_derive(input: TokenStream) -> TokenStream {
    eprintln!("Input tokens: {}", input);
    let ast = syn::parse2(input.clone().into()).unwrap();
    eprintln!("Parsed AST: {:#?}", ast);
    TokenStream::new()
}
23

Макросы

macro_rules!

macro_rules! определяет декларативные макросы. $x — захват, expr сопоставляет выражения. $(...)* повторяет. Макросы разворачиваются при компиляции. Полезны для уменьшения шаблонности. Стандартный макрос vec! работает аналогично.

rust
macro_rules! vec_of {
    ($($x:expr),*) => {{
        let mut v = Vec::new();
        $(v.push($x);)*
        v
    }};
}
let nums = vec_of!(1, 2, 3);

Процедурные макросы

Процедурные макросы генерируют код при компиляции. Три типа: derive (#[derive(Debug)]), attribute (#[my_attr]), function-like (my_macro!). Более мощные, чем macro_rules!, но требуют отдельный крейт. Используются serde, tokio и diesel.

rust
// In a separate crate with proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Generate impl HelloMacro for the type
    // Returns new TokenStream
}

Распространённые макросы

Встроенные макросы: println!/format! для вывода, vec! для векторов, assert!/assert_eq! для тестов, dbg! для отладки, todo!/unreachable! для управления потоком. Все основаны на macro_rules!. dbg! возвращает значение для цепочечного вызова.

rust
println!("Hello, {}!", "world");
format!("x = {}", 42);
vec![1, 2, 3];
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
dbg!(some_variable);  // Debug print
todo!("Not implemented");
unreachable!();

Гигиена макросов

Макросы Rust гигиеничны: идентификаторы, введённые макросом, не конфликтуют с идентификаторами в вызывающей области. Это предотвращает тонкие баги. temp внутри макроса отличается от внешнего temp. Декларативные макросы всегда гигиеничны.

rust
macro_rules! swap {
    ($a:expr, $b:expr) => {
        let temp = $a;
        $a = $b;
        $b = temp;
    };
}
// temp is hygienic: does not conflict with outer temp
let mut temp = 1;
let mut x = 2;
swap!(temp, x);  // Works correctly

Повторение

Повторение в макросах: $(...)* сопоставляет ноль или более, $(...)+ один или более. Разделитель (запятая) может быть указан. $x захватывает каждое значение. Полезно для вариативных функций. Стандартный println! использует это для нескольких аргументов.

rust
macro_rules! sum {
    ($($x:expr),*) => {
        0 $(+ $x)*
    };
}
let total = sum!(1, 2, 3, 4);  // 10
// $(...)* zero or more, $(...)+ one or more
// $(...),? optional trailing comma
24

Углублённое изучение Async

async/await

async fn возвращает Future. .await приостанавливается до готовности future. Future ленивы: ничего не запускается, пока не awaited. Компилятор преобразует async fn в конечный автомат. Используйте runtime tokio или async-std для выполнения.

rust
async fn fetch_data() -> String {
    // Simulate async work
    String::from("data")
}
async fn process() {
    let data = fetch_data().await;
    println!("{}", data);
}

Tokio Runtime

tokio::main включает async main. spawn создаёт задачу (подобно green thread). join! ждёт несколько future конкурентно. Tokio предоставляет I/O, таймеры и планирование. Самая популярная async-runtime в Rust.

rust
#[tokio::main]
async fn main() {
    let task1 = tokio::spawn(async { work1().await });
    let task2 = tokio::spawn(async { work2().await });
    let (r1, r2) = tokio::join!(task1, task2);
}

Каналы (async)

mpsc (multi-producer, single-consumer) каналы обеспечивают async-коммуникацию. send/recv async. Канал имеет буфер (32 сообщения). Когда все отправители drop'аются, recv возвращает None. Полезно для паттернов producer-consumer.

rust
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);
    tokio::spawn(async move {
        tx.send("hello").await.unwrap();
    });
    while let Some(msg) = rx.recv().await {
        println!("{}", msg);
    }
}

Select

select! ждёт первую из нескольких future. Другие future drop'аются. Полезно для таймаутов и соревнования операций. Паттерн распространён в сетевых серверах. Каждая ветка может иметь guard-паттерн.

rust
tokio::select! {
    result = task1 => {
        println!("Task1 done: {:?}", result);
    }
    result = task2 => {
        println!("Task2 done: {:?}", result);
    }
    _ = tokio::time::sleep(Duration::from_secs(5)) => {
        println!("Timeout");
    }
}

Stream

Stream — async-эквивалент Iterator. next().await получает следующий элемент. StreamExt предоставляет map, filter, for_each. Полезен для обработки чанков данных из сети или файлов. Крейт async-stream упрощает создание streams.

rust
use tokio_stream::{self as stream, StreamExt};
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(item) = stream.next().await {
    println!("{}", item);
}
// Map, filter like iterators but async
stream.map(|x| x * 2).filter(|x| *x > 2).for_each(|x| async move {
    println!("{}", x);
}).await;
25

Cargo и крейты

Cargo.toml

Cargo.toml — файл манифеста. [package] определяет метаданные. [dependencies] перечисляет внешние крейты. Features включают опциональную функциональность. [dev-dependencies] — только для тестов. Edition 2021 — последний стабильный. Версии используют semver.

rust
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
pretty_assertions = "1"

Команды Cargo

cargo new создаёт бинарный проект (--lib для библиотеки). build компилирует в target/. --release включает оптимизации. check быстрее build (без codegen). clippy отлавливает распространённые ошибки. fmt форматирует код. doc генерирует HTML-документацию. add вставляет зависимость.

rust
cargo new myapp        # Create new project
cargo build            # Compile
cargo build --release  # Optimized build
cargo run              # Build and run
cargo test             # Run tests
cargo check            # Fast type-check
cargo fmt              # Format code
cargo clippy           # Lint
cargo doc --open       # Generate docs
cargo add serde        # Add dependency

Workspace'ы

Workspace'ы группируют несколько крейтов, разделяющих целевую директорию и Cargo.lock. Члены — отдельные крейты. [workspace.dependencies] централизует версии зависимостей. Каждый крейт ссылается на них через workspace = true. Более быстрые сборки благодаря общей компиляции. Используется большими проектами вроде rust-analyzer.

rust
# Root Cargo.toml
[workspace]
members = ["crate-a", "crate-b"]

# Shared dependencies
[workspace.dependencies]
serde = "1.0"

# In crate-a/Cargo.toml
[dependencies]
serde = { workspace = true }

Features

Features включают условную компиляцию. features по умолчанию включены, если не --no-default-features. cfg(feature = ...) gates код. Синтаксис dep: в зависимостях избегает feature unification. Полезно для опциональной функциональности и платформо-специфичного кода. Крейты могут открывать features потребителям.

rust
# Cargo.toml
[features]
default = ["csv"]
csv = ["dep:csv-parse"]
json = ["dep:serde_json"]

# Conditional compilation
#[cfg(feature = "csv")]
pub fn parse_csv() { /* ... */ }

Публикация

crates.io — реестр пакетов Rust. cargo login аутентифицируется. --dry-run отлавливает проблемы. После публикации версию нельзя переиздать (yank только скрывает из поиска). Следуйте semver: patch для фиксов, minor для функций, major для breaking changes. README и license обязательны.

rust
# Login (one time)
cargo login <token>

# Check before publishing
cargo publish --dry-run

# Publish to crates.io
cargo publish

# Version bumping
cargo bump patch  # 0.1.0 -> 0.1.1
cargo bump minor  # 0.1.0 -> 0.2.0
cargo bump major  # 0.1.0 -> 1.0.0
26

Тестирование Rust

Unit-тесты

Тесты живут в модуле #[cfg(test)]. use super::* импортирует родителя. #[test] помечает тестовые функции. assert_eq! проверяет равенство. #[should_panic] ожидает панику. Тесты запускаются cargo test. Unit-тесты колокируются с кодом. Атрибут cfg(test) гарантирует, что тесты не компилируются в release-сборках.

rust
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
    #[test]
    #[should_panic]
    fn test_panic() {
        panic!("expected");
    }
}

Интеграционные тесты

Интеграционные тесты живут в директории tests/. Каждый файл компилируется как отдельный крейт. Они могут тестировать только публичный API. Полезны для end-to-end тестирования. Запускайте конкретные тесты через --test <name>. Интеграционные тесты медленнее компилируются, но тестируют реальный интерфейс.

rust
// tests/integration_test.rs
use myapp::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(2, 3), 5);
}
// Run: cargo test --test integration_test
// Each file in tests/ is a separate crate

Организация тестов

#[ignore] пропускает тест, если не передан --ignored. Фильтруйте тесты по шаблону имени. --nocapture показывает вывод println!. Тесты запускаются параллельно по умолчанию. Используйте --test-threads=1 для последовательного запуска. Кастомные harness'ы могут заменить дефолтный test runner. Полезно для бенчмарков и property-тестов.

rust
#[test]
fn it_works() { /* ... */ }

// Custom test harness
#[test]
#[ignore = "slow test"]
fn slow_test() { /* ... */ }

// Run only ignored tests
cargo test -- --ignored

// Filter by name
cargo test test_add

// Show output
cargo test -- --nocapture

Утверждения

assert! проверяет boolean. assert_eq!/assert_ne! сравнивают значения с debug-выводом при неудаче. Кастомные сообщения помогают отладке. Для floating point используйте крейт approx. Для частичного равенства реализуйте PartialEq. Debug-вывод показывает оба значения при неудаче assertion.

rust
assert!(true);                          // Boolean
assert_eq!(2 + 2, 4);                   // Equality
assert_ne!(3, 4);                       // Inequality
assert!(x > 0, "x must be positive");   // Custom message
assert_eq!(a, b, "got {}, expected {}", a, b);
// Debug output on failure
assert_eq!(vec![1, 2], vec![1, 2]);

Property-тестирование

proptest генерирует случайные входные данные для поиска падающих случаев. Strategies (a in range) определяют генераторы входов. prop_assert! сообщает неудачи с минимальными контрпримерами. Shrinking находит наименьший падающий вход. Лучше, чем рукописные тесты для краевых случаев. Похож на QuickCheck в Haskell.

rust
// Cargo.toml: proptest = "1"
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
        prop_assert_eq!(add(a, b), add(b, a));
    }
    #[test]
    fn test_string_len(s in ".{0,100}") {
        prop_assert!(s.len() <= 100);
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.