Основы
Переменные и изменяемость
Переменные Rust неизменяемы по умолчанию — это ключевая функция безопасности, предотвращающая случайные мутации. Используйте 'mut' только когда действительно нужно изменить значение. 'const' требует явного типа и встраивается при компиляции, а 'static' имеет фиксированный адрес в памяти со временем жизни 'static.
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 annotationShadowing
Shadowing позволяет переиспользовать имя переменной, изменяя её тип или значение. В отличие от 'mut', shadowing создаёт новую привязку — полезно для преобразования данных (например, парсинг строки в int) без придумывания нового имени. Старое значение уничтожается после новой привязки.
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.
// 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.
// 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.
// '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;Строки
String vs &str
Различие между String (владелец, куча) и &str (заимствованный срез) фундаментально в Rust. Используйте String, когда нужно владеть/изменять/наращивать текст; &str — когда нужно только читать. &str может указывать на буфер String или строковый литерал в бинарнике. Предпочитайте &str в параметрах функций для гибкости.
// &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).
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.
// 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().
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 из итератора символов.
// 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();Структуры данных
Массивы и срезы
Массивы [T; N] имеют фиксированный размер, известный при компиляции, и живут в стеке. Срезы &[T] — толстые указатели (указатель + длина), заимствующие непрерывную последовательность — они позволяют функциям принимать любой массив или вектор без учёта размера. Используйте срезы в сигнатурах функций для общности.
// 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 значения.
// 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 — возвращает изменяемую ссылку на значение, вставляя значение по умолчанию при отсутствии.
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, zebraHashSet и BTreeSet
HashSet хранит уникальные значения с O(1) проверкой принадлежности. Операции над множествами (union, intersection, difference, symmetric_difference) возвращают итераторы. Используйте множества для дедупликации, проверки принадлежности и математических операций над множествами. BTreeSet — отсортированный эквивалент, поддерживаемый BTreeMap.
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 в других языках. Кортежи часто используются для возврата нескольких значений из функций.
// 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)Управление потоком
If / Else If / Else
В отличие от многих языков, 'if' в Rust — выражение, возвращающее значение. Это устраняет необходимость в тернарном операторе (condition ? a : b) — просто используйте if/else. Обе ветки должны возвращать одинаковый тип. Условие НЕ требует скобок, но тело должно быть блоком { }.
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 инклюзивный.
// 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.