Структуры, перечисления и pattern matching
enum в Rust — это тип-сумма с данными внутри вариантов, а match обязан разобрать их все, иначе код не соберётся.
Pattern matching — сопоставление значения с образцами, при котором данные одновременно проверяются и распаковываются в переменные.
Вопрос: «Чем enum в Rust отличается от enum в C или Java?»
В C enum — это по сути именованное целое. В Rust это полноценный алгебраический тип-сумма: значение равно ровно одному варианту, и каждый вариант может нести собственные данные своей формы.
enum Message {
Quit, // без данных
Move { x: i32, y: i32 }, // как структура с полями
Write(String), // как кортежная структура
ChangeColor(u8, u8, u8),
}
fn handle(msg: Message) {
match msg {
Message::Quit => println!("выходим"),
Message::Move { x, y } => println!("сдвиг на {x}, {y}"),
Message::Write(text) => println!("текст: {text}"),
Message::ChangeColor(r, g, b) => println!("цвет {r} {g} {b}"),
}
}
fn main() {
handle(Message::Move { x: 3, y: -1 });
handle(Message::Write(String::from("привет")));
}
Вывод:
сдвиг на 3, -1 текст: привет
Размер такого enum — это тег плюс место под самый крупный вариант; данные не лежат по разным указателям, всё внутри одного значения. Здесь же уместно сказать, что Option<T> и Result<T, E> — не магия компилятора, а обычные enum из стандартной библиотеки:
enum Option<T> { None, Some(T) }
enum Result<T, E> { Ok(T), Err(E) }
Что на самом деле проверяет интервьюер
- Знаете ли, что варианты несут данные, — это отличает Rust от C-подобных перечислений.
- Понимаете ли, что null и «код ошибки» заменены обычными типами, которые нельзя молча проигнорировать.
- Не путаете ли enum со структурой: структура — «и то, и другое», enum — «либо одно, либо другое».
Вопрос: «Что такое исчерпывающий match и зачем он нужен?»
match обязан покрыть все возможные значения. Ценность этого правила видна не в момент написания кода, а через полгода — когда в enum добавляют новый вариант.
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
Triangle { base: f64, height: f64 }, // добавили позже
}
fn area(s: Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
}
}
Ошибка компиляции:
error[E0004]: non-exhaustive patterns: `Shape::Triangle { .. }` not covered
--> src/main.rs:8:11
|
8 | match s {
| ^ pattern `Shape::Triangle { .. }` not covered
|
note: `Shape` defined here
--> src/main.rs:1:6
|
1 | enum Shape {
| ^^^^^
...
4 | Triangle { base: f64, height: f64 },
| -------- not covered
= note: the matched value is of type `Shape`
help: ensure the match is exhaustive by adding a match arm with a pattern
Компилятор превратил «где-то забыли обработать новый случай» из ночного инцидента в ошибку сборки и сам показал все места, которые надо дописать. Именно поэтому арм _ вреден, если стоит по enum вашего проекта: он молча съест будущие варианты и вернёт что-нибудь неправильное. _ хорош там, где вариантов заведомо бесконечно много — например, при match по числам.
Что на самом деле проверяет интервьюер
Ждут не определения, а инженерного вывода: исчерпывающий match — это инструмент рефакторинга. Добавили вариант, запустили cargo check, обошли список ошибок — и уверены, что ничего не пропущено.
cargo check
Вопрос: «Как деструктурировать структуру в match?»
Тем же синтаксисом, что и создание, только вместо значений — образцы. Лишние поля закрываются двумя точками.
struct User {
id: u32,
name: String,
active: bool,
}
fn describe(u: User) -> String {
match u {
User { id: 1, .. } => String::from("это админ"),
User { name, active: true, .. } => format!("активен: {name}"),
User { name, .. } => format!("заблокирован: {name}"),
}
}
fn main() {
println!("{}", describe(User { id: 7, name: String::from("аня"), active: true }));
println!("{}", describe(User { id: 9, name: String::from("пётр"), active: false }));
}
Вывод:
активен: аня заблокирован: пётр
Guard, диапазоны и связывание @
fn describe_num(n: i32) -> String {
match n {
0 => String::from("ноль"),
x if x < 0 => format!("отрицательное {x}"),
d @ 1..=9 => format!("одна цифра: {d}"),
_ => String::from("большое число"),
}
}
Guard — это обычное условие после образца; оно не участвует в проверке исчерпывающести, поэтому один арм без guard всё равно нужен. Связывание d @ 1..=9 одновременно проверяет диапазон и кладёт само значение в переменную.
if let, let else, while let и matches!
Когда интересен один вариант, полный match — лишний шум.
let config: Option<u16> = Some(3000);
if let Some(port) = config {
println!("порт {port}");
}
fn parse_port(s: &str) -> u16 {
let Ok(port) = s.parse::<u16>() else {
return 8080; // ветка else обязана расходиться: return, break, panic!
};
port
}
fn main() {
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
println!("{}", matches!(parse_port("70000"), 8080));
}
Вывод:
3 2 1 true
let else оставляет «удачное» значение в основном потоке кода без лишней вложенности, а matches! — это макрос, который сводит сопоставление к bool и умеет принимать guard: matches!(x, Some(n) if n > 3).
Кортежные и unit-структуры
struct Meters(f64); // кортежная: newtype поверх f64
struct Marker; // unit-структура: данных нет вовсе
let dist = Meters(4.5);
let Meters(value) = dist; // деструктуризация в let
println!("{value}");
Кортежные структуры чаще всего используют как newtype: Meters(f64) и Seconds(f64) — разные типы, перепутать их компилятор не даст. Unit-структуры нужны как носители реализаций трейтов, когда состояние не требуется.
Типичные ошибки кандидатов
- Описывать enum как «список констант» и не упоминать данные в вариантах.
- Считать Option и Result встроенными в язык типами вместо обычных enum стандартной библиотеки.
- Закрывать match армом
_по своему enum и терять защиту от новых вариантов. - Думать, что guard учитывается при проверке исчерпывающести.
- Писать
match x { Some(v) => ..., None => () }вместоif let. - Забывать, что ветка
let ... elseобязана завершать поток управления, а не просто вычислять значение.
Как ответить кратко
enum в Rust — это тип-сумма: значение равно одному из вариантов, а варианты несут собственные данные, как Message::Move { x, y } или Shape::Circle(f64). Option и Result — просто такие же enum из стандартной библиотеки, поэтому null и коды ошибок не нужны. match обязан быть исчерпывающим: добавили вариант — получили E0004 во всех местах, где его забыли обработать, и это ловит баги при рефакторинге, поэтому армом _ по своему enum лучше не злоупотреблять. Деструктурировать можно структуры и кортежи прямо в образце, с .. для остальных полей, guard'ом для условий и @ для связывания, а для одного интересного варианта есть if let, let else, while let и matches!.