🦀 Rust
Полный справочник: cargo, типы, владение и заимствование, lifetimes, enum и match, трейты, дженерики, обработка ошибок, коллекции, итераторы, async, тесты и крейты.
Шпаргалки · Программирование · #rust #systems #memory-safety #cargo
Инструменты
rustup update rustc --version rustup component add clippy rustfmt rust-analyzer
cargo new app / cargo init cargo run [--release] cargo build --release cargo check # check быстрее build
cargo test [name] -- --nocapture cargo fmt cargo clippy -- -D warnings
cargo add serde --features derive cargo doc --open cargo tree cargo audit
cargo install cargo-watch cargo watch -x run
IDE: RustRover, VS Code + rust-analyzer. Редакции: 2021, 2024. Сайт крейтов: crates.io, документация: docs.rs.
Минимальная программа
fn main() {
println!("Привет, {}!", "мир");
}
Структура проекта: Cargo.toml (манифест), src/main.rs (или lib.rs), tests/, benches/.
Переменные и типы
let x = 5; // неизменяемая по умолчанию
let mut y = 1; y += 1; // изменяемая
let z: i64 = 10; let s = x as f64; // приведение явное
const MAX: u32 = 100; static NAME: &str = "app";
let (a, b) = (1, "a"); let [p, q, ..] = [1, 2, 3, 4];
let x = x * 2; // затенение (shadowing)
| Группа | Типы |
|---|---|
| Целые | i8…i128, u8…u128, isize, usize |
| Дробные | f32, f64 |
| Прочие | bool, char (4 байта, Unicode), () (unit) |
| Строки | &str (срез), String (владеющая) |
| Составные | кортеж (i32, f64), массив [i32; 3], срез &[i32], Vec<T> |
Переполнение целых: в debug паника, в release оборачивается. Явно: checked_add, wrapping_add, saturating_add, overflowing_add.
Владение и заимствование (главная идея Rust)
- У значения один владелец.
- Когда владелец выходит из области, значение освобождается (
Drop). - Присваивание или передача перемещает значение (для не-
Copyтипов). - Ссылки заимствуют: сколько угодно
&Tили одна&mut T.
let s1 = String::from("a");
let s2 = s1; // s1 перемещён, использовать нельзя
let s3 = s2.clone(); // явная копия
fn len(s: &String) -> usize { s.len() } // заимствование только для чтения
fn push(s: &mut String) { s.push('x'); } // изменяемое заимствование
let r1 = &s3; let r2 = &s3; // можно много &
// let m = &mut s3; println!("{r1}"); // ошибка: одновременно & и &mut
Copy-типы (числа, bool, char, кортежи из них) копируются. Срезы: &s[1..3], &v[..].
Времена жизни (lifetimes)
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b } }
struct Parser<'a> { text: &'a str }
Время жизни связывает ссылки результата и аргументов. Компилятор часто выводит их сам (elision).
Операторы и управление
if x > 0 { } else if x == 0 { } else { }
let kind = if n % 2 == 0 { "чёт" } else { "нечёт" }; // if — выражение
loop { break value; } while cond { } for i in 0..5 { } for i in (0..=5).rev() { }
for (i, v) in vec.iter().enumerate() { }
'outer: for a in 0..3 { for b in 0..3 { if b == 1 { continue 'outer; } } }
if let Some(v) = opt { } while let Some(x) = stack.pop() { } let Some(v) = opt else { return };
Операторы: + - * / %, == != < >, && || !, & | ^ << >>, as, ?, .. / ..=, &, *.
Структуры, enum, match
#[derive(Debug, Clone, PartialEq, Default)]
struct User { id: u32, name: String }
impl User {
fn new(id: u32, name: &str) -> Self { Self { id, name: name.to_string() } } // ассоциированная функция
fn label(&self) -> String { format!("@{}", self.name) } // метод
fn rename(&mut self, n: &str) { self.name = n.into(); }
}
let u = User { id: 2, ..User::default() }; struct Meters(f64); struct Unit;
enum Shape { Circle(f64), Rect { w: f64, h: f64 }, Empty }
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => 3.14 * r * r,
Shape::Rect { w, h } => w * h,
Shape::Empty => 0.0,
}
}
match n { 0 => "ноль", 1..=9 => "цифра", x if x < 0 => "минус", _ => "много" }
match обязан быть исчерпывающим. Option<T> (Some / None) заменяет null, Result<T, E> (Ok / Err) заменяет исключения.
Option и Result
let o: Option<i32> = Some(1);
o.map(|x| x + 1); o.unwrap_or(0); o.unwrap_or_default(); o.ok_or("нет"); o.and_then(f); o.is_some(); o.filter(|x| *x > 0);
let r: Result<i32, String> = Ok(1);
r.map_err(|e| e.to_string())?; r.unwrap_or_else(|e| 0); r.is_ok(); r.ok();
fn parse(s: &str) -> Result<i32, std::num::ParseIntError> {
let n: i32 = s.trim().parse()?; // ? возвращает ошибку наружу
Ok(n * 2)
}
unwrap() и expect() паникуют: оставляйте для тестов и «невозможных» случаев. Для приложений anyhow::Result, для библиотек thiserror:
#[derive(thiserror::Error, Debug)]
enum AppError { #[error("не найдено: {0}")] NotFound(String), #[error(transparent)] Io(#[from] std::io::Error) }
Трейты и дженерики
trait Shape { fn area(&self) -> f64; fn name(&self) -> String { "фигура".into() } }
impl Shape for Circle { fn area(&self) -> f64 { 3.14 * self.r * self.r } }
fn print_area<T: Shape>(s: &T) { } // статическая диспетчеризация
fn print_dyn(s: &dyn Shape) { } // динамическая (vtable)
fn make() -> impl Shape { Circle { r: 1.0 } }
fn largest<T: PartialOrd + Copy>(xs: &[T]) -> T where T: Copy { let mut m = xs[0]; for &x in xs { if x > m { m = x; } } m }
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { r: 1.0 })];
impl std::fmt::Display for User { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.name) } }
impl From<&str> for User { fn from(s: &str) -> Self { User::new(0, s) } }
Стандартные трейты: Clone, Copy, Debug, Display, Default, PartialEq, Eq, Hash, PartialOrd, Ord, From/Into, Iterator, Drop, Send, Sync.
Умные указатели
| Тип | Назначение |
|---|---|
Box<T> |
значение в куче |
Rc<T> / Arc<T> |
общее владение (в одном потоке / между потоками) |
RefCell<T> / Mutex<T> / RwLock<T> |
изменение через общую ссылку (проверка во время выполнения / с блокировкой) |
Cow<'a, T> |
копирование по требованию |
Типичные связки: Rc<RefCell<T>>, Arc<Mutex<T>>.
Коллекции и итераторы
let mut v = vec![3, 1, 2]; v.push(4); v.sort(); v.dedup(); v.iter().max(); v.len(); v[0]; v.get(10); v.contains(&2); v.retain(|x| *x > 1);
use std::collections::{HashMap, HashSet, BTreeMap, VecDeque};
let mut m = HashMap::new(); m.insert("a", 1); m.get("a"); *m.entry("b").or_insert(0) += 1; m.contains_key("a");
let sum: i32 = v.iter().sum();
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let evens: Vec<_> = v.iter().filter(|&&x| x % 2 == 0).cloned().collect();
v.iter().enumerate(); v.iter().zip(w.iter()); v.iter().fold(0, |a, x| a + x); v.iter().any(|&x| x > 2); v.iter().position(|&x| x == 2);
v.chunks(2); v.windows(2); v.iter().rev(); v.iter().take(3).skip(1); v.iter().flat_map(|x| vec![*x, *x]);
let words: Vec<&str> = text.split_whitespace().collect();
let by_len: HashMap<usize, Vec<&str>> = words.iter().fold(HashMap::new(), |mut m, w| { m.entry(w.len()).or_default().push(*w); m });
Итераторы ленивые: вычисления начинаются при collect, sum, for.
Строки
let s = String::from("Привет"); let t: &str = &s;
s.len(); // байты
s.chars().count(); // символы
s.push_str("!"); s.contains("р"); s.replace("а", "я"); s.trim(); s.to_uppercase(); s.split(',').collect::<Vec<_>>(); s.starts_with("П");
format!("{name}: {n:>5} {x:.2} {v:?} {v:#?}"); s.parse::<i32>(); s.chars().rev().collect::<String>(); &s[0..2] // индексы по байтам: паника на границе символа
Модули и видимость
mod geometry { pub struct Point { pub x: f64 } pub fn dist() {} mod private {} }
use geometry::{Point, dist}; use std::collections::HashMap; pub use self::inner::Thing; // реэкспорт
pub(crate) fn only_in_crate() {}
Файлы: mod name; ищет name.rs или name/mod.rs. Workspace: несколько крейтов под одним Cargo.toml.
Асинхронность и потоки
use std::thread; use std::sync::{Arc, Mutex, mpsc};
let handle = thread::spawn(|| 42); handle.join().unwrap();
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter); thread::spawn(move || { *c.lock().unwrap() += 1; });
let (tx, rx) = mpsc::channel(); tx.send(1).unwrap(); rx.recv().unwrap();
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let (a, b) = tokio::join!(fetch(1), fetch(2)); // параллельно
let r = tokio::time::timeout(std::time::Duration::from_secs(5), fetch(3)).await?;
let h = tokio::spawn(async { 1 + 1 }); h.await?;
Ok(())
}
async fn fetch(id: u32) -> reqwest::Result<String> { reqwest::get(format!("https://x.com/{id}")).await?.text().await }
Трейты Send и Sync гарантируют безопасность потоков на этапе компиляции: гонок данных в безопасном Rust нет. Для параллельных вычислений: rayon (par_iter()).
Макросы и атрибуты
println!, format!, vec!, assert!, assert_eq!, dbg!, todo!, unimplemented!, matches!. Атрибуты: #[derive(...)], #[cfg(test)], #[allow(...)], #[inline]. Собственные макросы: macro_rules! и процедурные (syn, quote).
unsafe
Блок unsafe { } разрешает разыменование сырых указателей, вызов unsafe функций и FFI. Держите его маленьким и оборачивайте в безопасный API.
Тесты
#[cfg(test)]
mod tests {
use super::*;
#[test] fn adds() { assert_eq!(add(1, 2), 3); }
#[test] #[should_panic(expected = "деление")] fn panics() { div(1, 0); }
#[test] fn result() -> Result<(), String> { Ok(()) }
}
Интеграционные тесты в tests/, бенчмарки criterion, фаззинг cargo fuzz, свойства proptest, примеры в документации (/// с кодом) тоже тесты.
Подводные камни
- Борьба с заимствованиями: клонируйте осознанно, разделяйте владение (
Rc,Arc), проектируйте данные без самоссылок. unwrap()в продакшене.- Индексация строк по байтам.
- Блокирующий код внутри
async(std::thread::sleep):tokio::time::sleep,spawn_blocking. - Удержание
MutexGuardчерез.await. - Долгая компиляция:
cargo check,sccache,mold, раздельные крейты.
Крейты по задачам
| Задача | Что взять |
|---|---|
| Async runtime | tokio, async-std, smol |
| Веб | axum, actix-web, rocket, warp |
| HTTP-клиент | reqwest, hyper, ureq |
| Сериализация | serde, serde_json, toml, bincode |
| CLI | clap, dialoguer, indicatif |
| БД | sqlx, diesel, sea-orm, rusqlite |
| Ошибки | anyhow, thiserror, eyre |
| Логи и трассировка | tracing, log + env_logger |
| Параллелизм | rayon, crossbeam, parking_lot |
| Время | chrono, time |
| Тесты | proptest, mockall, insta, criterion |
| Встроенные и ядро | embedded-hal, no_std, Embassy |