RustMiddleTechnical

Что такое pattern matching в Rust и как работает выражение match?

Pattern matching в Rust — механизм деструктуризации и ветвления, где компилятор статически проверяет полноту (exhaustiveness) покрытия всех вариантов. Выражение match сравнивает значение с паттернами последовательно и выполняет первый совпавший arm.

Базовый синтаксис match

Выражение match — это не оператор switch, а выражение, возвращающее значение. Компилятор требует покрытия всех возможных значений (exhaustive matching):

#[derive(Debug)]
enum HttpStatus {
    Ok,
    NotFound,
    InternalError(String),
    Redirect { code: u16, location: String },
}

fn describe(status: HttpStatus) -> String {
    match status {
        HttpStatus::Ok => String::from("200 OK"),
        HttpStatus::NotFound => String::from("404 Not Found"),
        HttpStatus::InternalError(msg) => format!("500: {}", msg),
        HttpStatus::Redirect { code, location } => {
            format!("{}  -> {}", code, location)
        }
    }
}

Если добавить новый вариант в enum и не обновить match — ошибка компиляции. Это ключевое преимущество перед switch/case в других языках.

Паттерны: полный арсенал

let x: i32 = 7;

match x {
    0 => println!("ноль"),
    1..=5 => println!("от 1 до 5"),      // range pattern
    6 | 7 | 8 => println!("6, 7 или 8"), // or pattern
    n if n > 100 => println!("большое: {}", n), // guard
    _ => println!("что-то другое"),      // wildcard
}

Деструктуризация структур и кортежей

struct Point { x: i32, y: i32 }

let p = Point { x: 3, y: -5 };

match p {
    Point { x: 0, y } => println!("на оси Y, y={}", y),
    Point { x, y: 0 } => println!("на оси X, x={}", x),
    Point { x, y } if x == y => println!("на диагонали"),
    Point { x, y } => println!("({}, {})", x, y),
}

// Кортежи
let pair = (true, 42);
match pair {
    (true, n) => println!("true и {}", n),
    (false, _) => println!("false"),
}

Binding (@) — захват с проверкой

let num = 15u32;

match num {
    n @ 1..=12 => println!("месяц {}", n),
    n @ 13..=19 => println!("подросток {}", n),
    n => println!("другое: {}", n),
}

if let и while let — сокращённые формы

// if let: когда интересует только один вариант
let config_port: Option<u16> = Some(8080);
if let Some(port) = config_port {
    println!("порт: {}", port);
}

// if let с else
if let Ok(n) = "42".parse::<u32>() {
    println!("число: {}", n);
} else {
    println!("не число");
}

// while let: читать из канала пока есть значения
use std::collections::VecDeque;
let mut stack: VecDeque<i32> = VecDeque::from([1, 2, 3]);
while let Some(top) = stack.pop_back() {
    println!("{}", top);
}

Паттерны в let и аргументах функций

// Деструктуризация в let
let (a, b, c) = (1, 2, 3);
let Point { x, y } = Point { x: 10, y: 20 };

// Деструктуризация в аргументах
fn print_point(&(x, y): &(i32, i32)) {
    println!("({}, {})", x, y);
}

// В for-цикле
let pairs = vec![(1, 'a'), (2, 'b')];
for (num, ch) in &pairs {
    println!("{}: {}", num, ch);
}

matches! — макрос для булевой проверки

let status = HttpStatus::NotFound;

// Вместо: matches!(status, HttpStatus::NotFound | HttpStatus::Ok)
let is_client_or_ok = matches!(status, HttpStatus::NotFound | HttpStatus::Ok);

// Полезно в filter:
let statuses = vec![HttpStatus::Ok, HttpStatus::NotFound, HttpStatus::Ok];
let ok_count = statuses.iter().filter(|s| matches!(s, HttpStatus::Ok)).count();

Подводные камни

  • Незакрытый wildcard _ скрывает ошибки: добавление _ => unreachable!() вместо перечисления вариантов означает, что новый вариант enum не вызовет ошибку компиляции — только панику в рантайме.
  • Перемещение в match: match по значению перемещает его. Если нужен доступ к оригиналу после match, используйте match &value или ref в паттерне.
  • Guard не участвует в exhaustiveness: n if n > 0 не закрывает все i32. После guard-паттерна нужен явный _ => или паттерн без guard.
  • Порядок arm имеет значение: паттерны проверяются сверху вниз. Более конкретные паттерны должны идти раньше широких, иначе они никогда не сработают (компилятор предупреждает).
  • Связывание в or-паттернах: в паттерне A(x) | B(x) переменная x должна иметь одинаковый тип в обоих вариантах, иначе ошибка компиляции.
  • Деструктуризация с ..: Point { x, .. } игнорирует остальные поля. Удобно, но при добавлении поля в структуру компилятор не напомнит обновить match.
  • match на строках: нельзя использовать String в паттернах напрямую — только &str. match s.as_str() { "foo" => ... }.
  • Вложенный match vs комбинаторы: глубокая вложенность match снижает читаемость. Для Option/Result предпочтительны map, and_then, ?.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics