Сопоставление с образцом в Rust
В уроке Match ты познакомился с основами: ветки, _, объединение значений через | и диапазоны. Но это только начало. Образец (pattern) в Rust умеет ещё и заглядывать внутрь значения, доставать нужные части и проверять дополнительные условия — и всё это в одной строке.
Представь образец как формочку для печенья. Ты прикладываешь её к значению. Если форма подходит, Rust отдаёт тебе кусочки, которые были внутри. Если не подходит — пробует следующую ветку. Когда это уложится в голове, ты начнёшь пользоваться образцами повсюду: в match, в let, в параметрах функций и в if let.
Условия-охранники: добавляем if
Заголовок раздела «Условия-охранники: добавляем if»Иногда одной формы мало и нужна ещё одна проверка. Условие-охранник (match guard) — это условие if, записанное после образца. Ветка выполняется, только если образец подходит и условие истинно.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "мороз", 0 => "ровно ноль", t if t < 25 => "комфортно", _ => "жарко", }; println!("{t}°C — {feeling}"); }}Вывод:
-5°C — мороз0°C — ровно ноль18°C — комфортно35°C — жаркоRust проверяет ветки сверху вниз, поэтому порядок важен. 18 не проходит первое условие, не равно 0 и проходит t < 25 — значит, «комфортно».
Деструктуризация кортежей и структур
Заголовок раздела «Деструктуризация кортежей и структур»Деструктуризация (destructuring) — это разбор значения на части. Для кортежа образец пишется в той же форме, что и сам кортеж. Для структуры перечисляешь нужные поля.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("начало координат"), (x, 0) => println!("на оси x в точке {x}"), (0, y) => println!("на оси y в точке {y}"), (x, y) => println!("где-то ещё: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("на оси y, y = {y}"), Point { x, y: 0 } => println!("на оси x, x = {x}"), Point { x, y } => println!("в точке ({x}, {y})"), }}Вывод:
где-то ещё: (3, -7)на оси y, y = 5Обрати внимание на смесь. 0 — фиксированное значение, оно должно совпасть в точности. x и y — новые переменные, которые ловят всё, что там лежит. Point { x, y } — это сокращение для Point { x: x, y: y }.
Деструктуризация перечислений с данными
Заголовок раздела «Деструктуризация перечислений с данными»Вот где образцы по-настоящему блистают. Вариант перечисления может нести данные, и образец распаковывает их, одновременно проверяя, какой это вариант.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("площадь = {:.2}", area(shape)); }}Вывод:
площадь = 12.57площадь = 13.50площадь = 2.25До radius не добраться, пока не доказано, что значение — это Circle. В этом и выигрыш в безопасности: компилятор не даст прочитать данные не того варианта.
Вложенные образцы и привязки @
Заголовок раздела «Вложенные образцы и привязки @»Образцы можно вкладывать друг в друга на любую глубину. Здесь перечисление Command содержит перечисление Direction, и одна ветка проверяет оба уровня сразу.
Привязка @ (binding) позволяет проверить значение по образцу и сохранить его в переменной. n @ 1..=9 значит «проверь, что значение от 1 до 9, и назови его n».
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("вверх на {steps} (маленький шаг)"), Command::Move(Direction::Up, steps) => println!("вверх на {steps} (большой прыжок!)"), Command::Move(Direction::Down, steps) => println!("вниз на {steps}"), Command::Say(text) => println!("говорю: {text}"), Command::Quit => println!("пока"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("привет"))); run(Command::Quit);}Вывод:
вверх на 3 (маленький шаг)вверх на 50 (большой прыжок!)вниз на 2говорю: приветпокаБез @ диапазон вроде 1..=9 проверяет значение, но тут же его выбрасывает. С @ ты его сохраняешь.
Пропускаем части с помощью _ и ..
Заголовок раздела «Пропускаем части с помощью _ и ..»Часто тебе нужны не все части. _ пропускает одно значение, а .. — «всё остальное».
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("красный = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("первое = {first}, последнее = {last}");
let hero = Player { name: String::from("Мира"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name}: уровень {level}");}Вывод:
красный = 255первое = 1, последнее = 5Мира: уровень 7.. может встретиться в образце только один раз. (first, .., last) — можно, а (.., middle, ..) — нельзя: Rust не поймёт, какое значение ты имел в виду.
Образцы в let и параметрах функций
Заголовок раздела «Образцы в let и параметрах функций»Каждый let на самом деле — образец. И каждый параметр функции тоже. Значит, разбирать значение можно прямо там, где оно появляется.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Вывод:
1920x1080x: 4, y: -2[3, 7, 11]Замыкание |&(a, b)| a + b использует ту же идею. Каждый элемент — это &(i32, i32), и образец сразу распаковывает его в a и b. С итераторами ты будешь встречать это постоянно.
matches! и let-else
Заголовок раздела «matches! и let-else»Два удобных инструмента избавляют от необходимости писать полный match.
matches!(значение, образец) возвращает true или false. Он идеален внутри if или .filter(), когда нужен только ответ «да/нет». Условия-охранники здесь тоже работают.
let-else решает задачу «распакуй это или уходи». Пишется так: let ОБРАЗЕЦ = значение else { ... };. Если образец подходит, переменные доступны до конца функции. Если нет — выполняется блок else, и он обязан выйти: через return, break, continue или панику.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' — это не число"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("сдали: {passed}");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("тридцать"));}Вывод:
trueсдали: 3Some(31)'тридцать' — это не числоNonelet-else сохраняет «счастливый путь» плоским. Вместо того чтобы вкладывать код внутрь if let ... { }, ты сначала разбираешься с плохим случаем и идёшь дальше. Особенно хорошо это работает со значениями Option и Result.
Частые ошибки
Заголовок раздела «Частые ошибки»Условия-охранники не учитываются при проверке «все случаи покрыты». Компилятор не может рассуждать о твоих условиях if, поэтому этот код не скомпилируется, хотя два условия покрывают все числа:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("отрицательное"), x if x >= 0 => println!("ноль или положительное"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityКомпилятор сообщает, что образцы неполные: диапазон i32::MIN..=i32::MAX не покрыт, потому что ветки с охранниками не засчитываются. Исправление: сделай последнюю ветку обычной заглушкой, например _ => println!("ноль или положительное").
Имя переменной в образце — это новая переменная, а не сравнение. Кажется, что здесь проверяется guess == expected, но это не так:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Correct!"), _ => println!("Wrong"), }}Код компилируется и выводит Correct!, потому что expected в ветке просто создаёт новую переменную, которая ловит любое значение. Rust предупреждает об этом:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Correct!"), | -------- matches any value6 | _ => println!("Wrong"), | ^ no value can reach thisПредупреждение говорит: ветка expected подходит под любое значение, поэтому до _ дело никогда не дойдёт. (Строки здесь оставлены на английском, потому что компилятор цитирует эти строки кода целиком.) Исправление — условие-охранник: g if g == expected => println!("Correct!").
Практика
Заголовок раздела «Практика»1. FizzBuzz с кортежем
Заголовок раздела «1. FizzBuzz с кортежем»Выведи числа от 9 до 15, но вместо чисел, кратных 3, пиши Fizz, вместо кратных 5 — Buzz, а вместо кратных и тому и другому — FizzBuzz. Сделай это одним match по кортежу вместо цепочки if. Подсказка: сопоставляй (n % 3, n % 5). Остаток 0 значит «делится нацело», а _ — «неважно».
Показать решение
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Вывод:
FizzBuzz11Fizz1314FizzBuzz(0, 0) должна стоять первой. Если бы первой была (0, _), для 15 вывелось бы Fizz.
2. Банковские операции
Заголовок раздела «2. Банковские операции»Объяви enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Начни с balance, равного 100, и примени по порядку такой список: пополнение на 50, снятие 30, снятие 500, комиссия. Комиссия стоит 5. Снятие суммы больше баланса нужно отклонить с сообщением. Выведи итоговый баланс. Подсказка: используй условие-охранник Transaction::Withdraw(amount) if amount <= balance, а за ним — вторую ветку Withdraw для отказа.
Показать решение
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("отказ: нельзя снять {amount}"), Transaction::Fee => balance -= 5, } }
println!("баланс = {balance}");}Вывод:
отказ: нельзя снять 500баланс = 115У второй ветки Withdraw нет охранника, поэтому она ловит каждое снятие, которое отклонила первая. Без неё match не скомпилируется: при проверке, что все случаи покрыты, охранники не учитываются.
- Условие-охранник (
образец if условие) добавляет к ветке дополнительную проверку. - Образцы умеют разбирать кортежи, структуры и варианты перечислений, и их можно вкладывать друг в друга.
имя @ образецпроверяет значение и одновременно сохраняет его в переменной._пропускает одно значение,..— всё остальное. Образцы работают и вlet, и в параметрах функций, и в замыканиях.matches!быстро даётtrue/false; let-else распаковывает значение или досрочно выходит.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что делает n @ 1..=9 в ветке match?
2. Что обязан сделать блок else в let-else?
3. Что даст let (first, .., last) = (1, 2, 3, 4, 5);?
4. Почему match, где у всех веток есть охранники, часто не компилируется?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Шаблоны и сопоставление — книга «Язык программирования Rust»: официальная глава обо всех местах, где встречаются образцы, и о том, когда они могут не совпасть (перевод на русский).
- Синтаксис шаблона — книга «Язык программирования Rust»: обзор всех форм образцов — от диапазонов и деструктуризации до охранников и привязок
@(перевод на русский). - Staying on the Happy Path with let…else — The Rust Book (на английском): спокойный разобранный пример let-else.
- Деструктуризация — Rust на примерах: запускаемые примеры деструктуризации кортежей, массивов, перечислений, указателей и структур (перевод на русский).
matches!macro — документация std (на английском): точный синтаксис макроса, в том числе с условием-охранником.