Перейти к содержимому

Сопоставление с образцом в Rust

В уроке Match ты познакомился с основами: ветки, _, объединение значений через | и диапазоны. Но это только начало. Образец (pattern) в Rust умеет ещё и заглядывать внутрь значения, доставать нужные части и проверять дополнительные условия — и всё это в одной строке.

Представь образец как формочку для печенья. Ты прикладываешь её к значению. Если форма подходит, Rust отдаёт тебе кусочки, которые были внутри. Если не подходит — пробует следующую ветку. Когда это уложится в голове, ты начнёшь пользоваться образцами повсюду: в match, в let, в параметрах функций и в if let.

Иногда одной формы мало и нужна ещё одна проверка. Условие-охранник (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 на самом деле — образец. И каждый параметр функции тоже. Значит, разбирать значение можно прямо там, где оно появляется.

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);
}

Вывод:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ Попробуй сам

Замыкание |&(a, b)| a + b использует ту же идею. Каждый элемент — это &(i32, i32), и образец сразу распаковывает его в a и b. С итераторами ты будешь встречать это постоянно.

Два удобных инструмента избавляют от необходимости писать полный 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
сдали: 3
Some(31)
'тридцать' — это не число
None
▶ Попробуй сам

let-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 value
6 | _ => println!("Wrong"),
| ^ no value can reach this

Предупреждение говорит: ветка expected подходит под любое значение, поэтому до _ дело никогда не дойдёт. (Строки здесь оставлены на английском, потому что компилятор цитирует эти строки кода целиком.) Исправление — условие-охранник: g if g == expected => println!("Correct!").

Выведи числа от 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}"),
}
}
}

Вывод:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Попробуй сам

(0, 0) должна стоять первой. Если бы первой была (0, _), для 15 вывелось бы Fizz.

Объяви 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