Структура группирует связанные значения в единый именованный тип. Rust поддерживает три формы: обычные структуры с именованными полями, кортежные структуры (tuple structs) и unit-структуры без полей.
#[derive(Debug, Clone)]
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
// Кортежная структура — поля без имён, доступ через .0, .1
struct Point3D(f64, f64, f64);
// Unit-структура — не хранит данных, используется как маркер/тег для трейтов
struct Marker;
fn main() {
let user1 = User {
username: String::from("ferris"),
email: String::from("ferris@rust-lang.org"),
active: true,
sign_in_count: 1,
};
// Struct update syntax: остальные поля берутся из user1
let user2 = User {
email: String::from("new@rust-lang.org"),
..user1 // username, active, sign_in_count скопированы/перемещены из user1
};
let origin = Point3D(0.0, 0.0, 0.0);
println!("{} {:?}", origin.0, user2);
}
..user1 перемещает поля username (String, не Copy) из user1 в user2. После этого user1 целиком становится частично перемещённым (partially moved) и недоступным для использования целиком, хотя поле active (Copy) технически ещё можно было бы прочитать по отдельности.
Поведение, связанное со структурой, определяется в отдельном блоке impl — в отличие от классов в ООП-языках, данные и методы физически разделены. Методы принимают self в одной из трёх форм; ассоциированные функции (associated functions) self не принимают вовсе и вызываются через ::.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Ассоциированная функция — конструктор ("статический метод")
fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
// Метод с неизменяемым заимствованием self — только чтение
fn area(&self) -> u32 {
self.width * self.height
}
// Метод с изменяемым заимствованием self — мутация полей
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
// Метод, забирающий владение self — типично для builder-паттерна/финализации
fn into_square(self) -> Self {
let side = self.width.max(self.height);
Self::new(side, side)
}
}
fn main() {
let mut rect = Rectangle::new(10, 20);
println!("area = {}", rect.area());
rect.scale(2);
let square = rect.into_square(); // rect перемещён, больше не используется
}
Один тип может иметь несколько impl-блоков (это активно используется для разделения обычных методов и реализаций трейтов), и все они компилируются как единая область.
В отличие от enum в C/Java (просто именованные константы), enum в Rust — это алгебраический тип данных (ADT): каждый вариант может нести собственные данные разных форм. Это делает enum мощным инструментом моделирования состояний, где "невозможные" комбинации данных просто не выразимы в системе типов.
#[derive(Debug)]
enum Message {
Quit, // без данных
Move { x: i32, y: i32 }, // именованные поля, как struct
Write(String), // один неименованный параметр
ChangeColor(i32, i32, i32), // несколько параметров, как tuple struct
}
impl Message {
fn call(&self) {
println!("{self:?}");
}
}
fn main() {
let messages = vec![
Message::Quit,
Message::Move { x: 10, y: 20 },
Message::Write(String::from("hello")),
Message::ChangeColor(255, 0, 0),
];
for m in &messages {
m.call();
}
}
Одной структурой такое смоделировать нельзя без опциональных полей и риска невалидных комбинаций (например, "Quit с координатами x/y"); enum же гарантирует на уровне компилятора, что данные всегда соответствуют варианту.
В Rust нет null. Отсутствие значения выражается стандартным generic enum'ом Option<T>, а операции, которые могут завершиться ошибкой — Result<T, E>. Оба типа — обычные enum'ы из стандартной библиотеки, никакой особой магии компилятора.
enum Option<T> {
None,
Some(T),
}
enum Result<T, E> {
Ok(T),
Err(E),
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
fn find_user(id: u32) -> Option<String> {
if id == 1 { Some(String::from("ferris")) } else { None }
}
Ключевое преимущество: компилятор заставляет обработать оба случая (Some/None, Ok/Err) — забытая проверка на null, которая в Java/Go/C всплывает NullPointerException/panic в рантайме, в Rust становится ошибкой компиляции, если не обработана явно.
match сравнивает значение с рядом паттернов и выполняет код для первого совпавшего. Компилятор требует исчерпываемости (exhaustiveness) — все возможные варианты должны быть покрыты, иначе — ошибка компиляции.
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String), // название штата на монете
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("Quarter from {state}!");
25
}
// ни одна ветка не пропущена — если добавить новый вариант в Coin,
// компилятор укажет здесь на ошибку "non-exhaustive match"
}
}
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1..=9 => "single digit", // диапазон
n if n < 0 => "negative", // match guard
_ => "large", // catch-all, обязателен без диапазона до i32::MAX
}
}
match, который нужно обновить. Это превращает рефакторинг enum'ов из "найти все места руками и не забыть ни одного" в "исправить всё, что укажет компилятор".
Когда важен только один вариант из многих, полный match избыточен — if let позволяет сопоставить один паттерн лаконично, жертвуя исчерпываемостью.
fn main() {
let config_value: Option<u8> = Some(3);
// Вместо match с двумя ветками, одна из которых пустая:
if let Some(max) = config_value {
println!("max = {max}");
} else {
println!("no config");
}
// while let — выполняется, пока паттерн совпадает
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}"); // 3, 2, 1
}
// let-else (Rust 1.65+): ранний выход, если паттерн не совпал
let Some(value) = config_value else {
return;
};
println!("value = {value}");
}
| match | if let | |
|---|---|---|
| Исчерпываемость | Обязательна (все ветки) | Не требуется — остальное игнорируется |
| Применение | Нужна обработка всех вариантов | Интересен ровно один паттерн |
| Читаемость при 1 варианте | Многословно | Лаконично |
struct Point { x: i32, y: i32 }
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p; // деструктуризация структуры в переменные
// Деструктуризация с точным сопоставлением и остатком
match p {
Point { x: 0, y } => println!("on y-axis at {y}"),
Point { x, y: 0 } => println!("on x-axis at {x}"),
Point { x, y } => println!("at ({x}, {y})"),
}
// Деструктуризация кортежей и вложенных enum'ов
let ((feet, inches), Point { x, .. }) = ((3, 10), p); // .. игнорирует остальные поля
// Связывание с @ — проверить паттерн И сохранить значение
let num = 5;
match num {
n @ 1..=5 => println!("got small number {n}"),
_ => println!("other"),
}
// Множественные паттерны через |
match num {
1 | 3 | 5 | 7 | 9 => println!("odd single digit"),
_ => println!("other"),
}
}
Макрос matches! удобен для проверки соответствия паттерну без полноценного match, когда нужен просто bool:
fn main() {
let opt = Some(4);
let is_present = matches!(opt, Some(_));
let in_range = matches!(opt, Some(n) if n > 0 && n < 10);
println!("{is_present} {in_range}");
}
Для распространённых операций над Option/Result стандартная библиотека предоставляет методы-комбинаторы, которые в Senior-коде почти всегда предпочтительнее ручного match.
fn main() {
let maybe_num: Option<i32> = Some(4);
let doubled = maybe_num.map(|n| n * 2); // Option<i32> -> Option<i32>
let value = maybe_num.unwrap_or(0); // значение или дефолт
let value2 = maybe_num.unwrap_or_else(|| expensive_default());
let value3 = maybe_num.unwrap_or_default(); // требует Default
let chained: Option<i32> = maybe_num.and_then(|n| {
if n > 0 { Some(n * 10) } else { None }
});
let res: Result<i32, String> = Ok(10);
let mapped = res.map(|v| v + 1).map_err(|e: String| e.to_uppercase());
println!("{doubled:?} {value} {value2} {value3} {chained:?} {mapped:?}");
}
fn expensive_default() -> i32 { 42 }
.unwrap() и .expect() вызывают panic при None/Err. Уместны в прототипах, тестах и там, где инвариант гарантирован логикой (с обязательным комментарием почему). В библиотечном и сервисном коде предпочитайте явную обработку через match, комбинаторы или оператор ? (тема 06).