исчерпывающее ветвление по форме значенияexhaustive branching on a value's shape

Pattern matchingPattern matching

match — основная конструкция ветвления в Rust: она разбирает значение по форме/варианту, а компилятор требует исчерпывающего покрытия всех случаев. Паттерны деструктурируют структуры, enum'ы, кортежи и ссылки прямо в ветвях match, а guard-условия и `@`-биндинги добавляют точность за пределами одной только формы.match is Rust's primary control-flow construct: it branches on a value's shape/variant, and the compiler requires every case to be exhaustively covered. Patterns destructure structs, enums, tuples, and references directly in match arms, while guard conditions and `@`-bindings add precision beyond shape alone.

основа языкаcore of the language частый вопрос: чем match отличается от switch и что такое исчерпываемостьcommon question: how match differs from switch and what exhaustiveness means

Что этоWhat it is

match сопоставляет значение с последовательностью паттернов и выполняет тело первой подошедшей ветви. Паттерн может деструктурировать значение прямо на месте: enum-вариант с данными, поля структуры, элементы кортежа, содержимое ссылки. К форме можно добавить дополнительное условие — match guard (if условие после паттерна) — для случаев, которые не выражаются одной лишь формой. Компилятор проверяет, что все возможные случаи покрыты (исчерпываемость), на этапе компиляции — непокрытый случай без wildcard-ветви _ не компилируется. Паттерны используются и вне match: в let-биндингах, параметрах функций, а также в if let/while let для лаконичной обработки одного ожидаемого варианта.match compares a value against a sequence of patterns and runs the body of the first arm that fits. A pattern can destructure the value right there: an enum variant with data, a struct's fields, a tuple's elements, the contents behind a reference. A pattern can be narrowed further with a match guard (an if condition after the pattern) for cases that shape alone can't express. The compiler checks that every possible case is covered (exhaustiveness) at compile time — an uncovered case with no wildcard arm _ fails to compile. Patterns also show up outside match: in let bindings, function parameters, and if let/while let for concisely handling a single expected variant.

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Point,
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { width, height } => width * height,
        Shape::Point => 0.0,  // без этой ветви — ОШИБКА КОМПИЛЯЦИИ: match не исчерпывающий// without this arm — COMPILE ERROR: match is not exhaustive
    }
}

Зачем ввелиWhy it was introduced

C/C++'s switch решает похожую задачу гораздо слабее — вот в чём разница:C/C++'s switch solves a similar problem far more weakly — here's the difference:

01

switch работает только с целочисленными/enum-значениями, без деструктуризацииswitch only works on integral/enum values, with no destructuring

C++'s switch сравнивает единственное скалярное значение с константами — он не умеет заглянуть внутрь struct, не умеет сопоставлять enum-вариант с данными (в C++ enum и не может нести данные), не умеет деструктурировать кортеж или ссылку прямо в ветви. Rust's match делает всё это как часть самого паттерна.C++'s switch compares a single scalar value against constants — it can't look inside a struct, can't match an enum variant carrying data (in C++ an enum can't carry data at all), can't destructure a tuple or a reference right in the arm. Rust's match does all of this as part of the pattern itself.

02

switch не проверяет исчерпываемость по умолчаниюswitch does not enforce exhaustiveness by default

Добавление нового варианта в C++ enum и забытый соответствующий case в существующем switch молча компилируется — если только не включён -Wswitch и не превращён в ошибку, а во многих кодовых базах это не так. Это реальный, повторяющийся класс багов при эволюции набора вариантов enum со временем.Adding a new variant to a C++ enum and forgetting a corresponding case in an existing switch silently compiles — unless -Wswitch is enabled and treated as an error, which many codebases don't do. This is a real, recurring class of bugs when an enum's variant set evolves over time.

03

match превращает исчерпываемость в обязательную ошибку компиляцииmatch turns exhaustiveness into a hard compile error

Rust делает исчерпываемость жёсткой ошибкой компиляции, а не опциональным предупреждением линтера. В сочетании с sum-type enum'ами (structs-enums.html) это означает: "добавь вариант enum — и любой match, который его не обрабатывает, перестаёт компилироваться, пока ты его не обработаешь" — превращая целый класс багов в forcing function на уровне компиляции по всей кодовой базе.Rust makes exhaustiveness a hard compile error, not an optional linter warning. Combined with sum-type enums (structs-enums.html), this means: "add an enum variant, and every match that doesn't handle it stops compiling until you do" — turning an entire bug class into a compile-time forcing function across a whole codebase.

Как это работает под капотомHow it works under the hood

if let — это синтаксический сахар над match, который заботится только об одном варианте и игнорирует остальные (с необязательным else). while let — то же самое, но в виде цикла, повторяющегося, пока паттерн продолжает подходить.if let is syntactic sugar over a match that cares about exactly one variant and ignores the rest (with an optional else). while let is the same idea as a loop, repeating for as long as the pattern keeps matching.

let maybe_value: Option<i32> = Some(7);

// полный match ради одного интересующего варианта — многословно// a full match for just one variant of interest — verbose
match maybe_value {
    Some(x) => println!("got {}", x),
    None => {}
}

// if let — то же самое, короче// if let — the same thing, shorter
if let Some(x) = maybe_value {
    println!("got {}", x);
}

Match guard добавляет к паттерну произвольное булево условие — паттерн должен подойти по форме и условие должно быть истинным, чтобы ветвь сработала. Паттерн @ одновременно проверяет форму/диапазон и связывает всё значение с именем.A match guard adds an arbitrary boolean condition to a pattern — the pattern must fit the shape and the condition must hold for the arm to fire. The @ pattern simultaneously tests a shape/range and binds the whole value to a name.

let pair = (3, 3);
match pair {
    (x, y) if x == y => println!("equal"),
    (x, _) if x > 0 => println!("first is positive"),
    _ => println!("other"),
}

let n = 4;
match n {
    n @ 1..=5 => println!("small: {}", n),  // n связан с самим значением диапазона// n is bound to the matched range value itself
    _ => println!("other"),
}

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Исчерпывающий match по HTTP-статусу и компилятор, ловящий забытый вариантExhaustive match over an HTTP-status-like enum, with the compiler catching a forgotten variant

До добавления варианта match компилируется. После добавления TooManyRequests в enum, старый match (написанный до этого изменения) перестаёт компилироваться с конкретной ошибкой — компилятор буквально не даёт забыть обработать новый случай нигде в кодовой базе.Before adding a variant, the match compiles. After adding TooManyRequests to the enum, the old match (written before that change) stops compiling with a specific error — the compiler literally will not let you forget to handle the new case anywhere in the codebase.

enum Status { Ok, NotFound, ServerError }

fn describe(s: &Status) -> &str {
    match s {
        Status::Ok => "ok",
        Status::NotFound => "not found",
        Status::ServerError => "server error",
    }
}

// позже кто-то добавляет вариант:// later someone adds a variant:
// enum Status { Ok, NotFound, ServerError, TooManyRequests }// enum Status { Ok, NotFound, ServerError, TooManyRequests }
// ОШИБКА: non-exhaustive patterns: `TooManyRequests` not covered// ERROR: non-exhaustive patterns: `TooManyRequests` not covered
// — функция describe отказывается компилироваться, пока ветвь не добавлена// — describe refuses to compile until the arm is added
02

if let/while let ради одного интересующего варианта Option/Resultif let/while let for just the variant of Option/Result you care about

Когда важен только Some/Ok, а остальные случаи не нужно явно разбирать здесь, if let избавляет от шаблонного кода полного match. Подробнее про Option/Result — в error-handling.html.When only Some/Ok matters and the other cases don't need explicit handling here, if let removes the boilerplate of a full match. More on Option/Result in error-handling.html.

let result: Result<i32, String> = parse_input();

if let Ok(value) = result {
    println!("parsed: {}", value);
}

let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("popped {}", top);  // повторяется, пока pop() возвращает Some// repeats as long as pop() returns Some
}
03

Деструктуризация структуры прямо в let-биндинге и в параметре функцииDestructuring a struct directly in a let binding and a function parameter

Паттерн не обязан участвовать в match — тот же деструктурирующий синтаксис работает в let и в позиции параметра функции, извлекая поля без промежуточных обращений через точку.A pattern doesn't have to be part of a match — the same destructuring syntax works in let and in a function parameter position, pulling out fields without intermediate dot-access.

struct Point { x: f64, y: f64 }

let point = Point { x: 3.0, y: 4.0 };
let Point { x, y } = point;  // деструктуризация в let, а не через point.x/point.y// destructuring in a let, instead of point.x/point.y
println!("{} {}", x, y);

fn distance(Point { x, y }: Point) -> f64 {
    (x * x + y * y).sqrt()  // поля доступны сразу, без point.x внутри тела// fields are available directly, no point.x inside the body
}
04

Match guard с диапазонами для бакетизации оценокA match guard with range patterns for grade bucketing

Диапазонные паттерны с @-биндингом одновременно проверяют попадание в диапазон и связывают значение с именем — заметно выразительнее цепочки if/else-if с теми же условиями.Range patterns with an @ binding simultaneously check range membership and bind the value to a name — noticeably more expressive than an if/else-if chain doing the same thing.

fn grade(score: u32) -> &'static str {
    match score {
        n @ 90..=100 => "A",
        n @ 80..=89 => "B",
        n @ 70..=79 => "C",
        _ => "F",
    }
}

ГраблиGotchas

Побеждает первый подходящий паттерн сверху внизThe first matching pattern top-to-bottom wins

Ветви match проверяются по порядку сверху вниз, и срабатывает ПЕРВЫЙ подходящий паттерн — слишком широкий ранний паттерн может незаметно перекрыть более специфичный, идущий дальше, который программист рассчитывал тоже сделать достижимым. Компилятор действительно предупреждает о заведомо недостижимых паттернах во многих случаях, но тонко пересекающиеся guard-условия по-прежнему могут давать неожиданный результат, который компилятор не всегда способен обнаружить.Match arms are checked top-to-bottom, and the FIRST matching pattern wins — an overly broad early pattern can silently shadow a more specific later one that the programmer intended to also be reachable. The compiler does warn about genuinely unreachable patterns in many cases, but subtly overlapping guard conditions can still produce surprising results that the compiler can't always detect.

Связывание значения в ветви по умолчанию перемещает егоBinding a value in an arm moves it by default

Связывание значения внутри паттерна ветви (например, Some(x) => ...) ПЕРЕМЕЩАЕТ значение из сопоставляемого выражения по умолчанию — если только тип не Copy, или сопоставление не идёт по ссылке, или не используется ref/эргономика сопоставления по ссылке. Извлечь элемент Vec<String> из Option<Vec<String>> по значению внутри ветви match, когда на самом деле нужен был только просмотр, — частый способ случайно переместить данные, которые ещё нужны после match.Binding a value inside a match arm pattern (e.g. Some(x) => ...) MOVES the value out of the matched-on expression by default — unless the type is Copy, or the match is on a reference, or ref/pattern-matching ergonomics are used. Matching a Vec<String>'s element out of an Option<Vec<String>> by value inside a match arm, when you only meant to inspect it, is a common way to accidentally move data you still needed afterward.

Guard-условия не засчитываются проверкой исчерпываемостиMatch guards are not counted by the exhaustiveness checker

Match guard (if условие на ветви) не считается проверкой исчерпываемости как "покрывающий" случай — match с Some(x) if x > 0 => ..., Some(x) if x <= 0 => ... по-прежнему требует конечной ветви None => ... или wildcard, даже если две ветви с guard'ами логически выглядят покрывающими все случаи Some, — компилятор в общем случае не может доказать исчерпываемость произвольных условий. Это часто сбивает с толку на старте.A match guard (if condition on an arm) is NOT considered by the exhaustiveness checker as "covering" a case — a match with Some(x) if x > 0 => ..., Some(x) if x <= 0 => ... still requires a trailing None => ... or wildcard arm, even though the two guarded arms look like they logically cover all cases of Some, because the compiler cannot generally prove guard conditions are exhaustive. This trips people up early on.