Структуры, перечисления и 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!.

Проверьте себя
1. Главное отличие enum в Rust от enum в C?
AВ Rust enum всегда занимает 8 байт
BВ Rust каждый вариант может нести собственные данные, то есть это тип-сумма
CВ Rust варианты нельзя сравнивать между собой
DВ Rust enum хранится в куче, а в C — на стеке
2. Чем полезна ошибка E0004 non-exhaustive patterns при добавлении нового варианта enum?
AОна заставляет добавить арм _ в каждый match
BОна отключает оптимизации до исправления кода
CОна превращает пропущенный случай в ошибку сборки и показывает все места, требующие правки
DОна проверяет, что варианты перечислены в том же порядке, что и в объявлении
3. Какое требование предъявляется к ветке else в конструкции let ... else?
AОна обязана вернуть значение того же типа, что и образец
BОна обязана завершить поток управления: return, break, continue или panic!
CОна может быть только пустой
DОна выполняется всегда, независимо от результата сопоставления