Введение#
Паттерн-матчинг в Rust часто воспринимают как аналог оператора switch/case из других языков. На деле это один из самых полезных инструментов в языке. Для нас авторы языка позаботились добавить выражения match, if let и let которые умеют разбирать структуры прямо на лету, проверять условия без вложенных if и отсекать невалидные состояния ещё на этапе компиляции.
В прошлых статьях я разбирал работу с enum rust-enums и управляющими конструкциями (типичные if/else) rust-conditionals.
Сегодня давайте отойдём от базовых примеров и разберём, как сопоставление с образцом (Pattern Matching) работает на практике: от @-биндингов в CLI-утилитах до разбора срезов и конечных автоматов в реальных open-source проектах. Я постарался добавить побольше наглядных примеров с кодом.
1. Принцип полноты и деструктуризация#
Главное требование компилятора к любому match — полнота проверок (exhaustiveness). Если тип может принять три состояния, вы обязаны явно обработать все три, либо поставить такую заглушку _ которую можно воспринимать, как «ну и всё остальное не попадающее под прошлые варианты» (аналог default).
В слайдах ниже на первом шаге показано, как компилятор пресекает попытку оставить вариант необработанным, а на втором - как сопоставление закрывается с помощью заглушки _ (хотя злоупотреблять ей не стоит, чтобы не пропустить добавления новых состояний в будущем):
Разбор структур и кортежей на лету#
Сопоставление работает не только в match, но и в обычных let- выражениях. Можно извлекать нужные поля из глубоко вложенных структур за одно действие:
struct Address {
street: String,
city: String,
}
struct Person {
name: String,
address: Address,
}
// Достаём имя и поля вложенного адреса сразу:
let Person {
name,
address: Address { street, city }
} = person;Лишние поля можно отбросить с помощью _ или оператора остатка ..:
let Person { name, address: Address { city, .. } } = person;Заимствование: Match Ergonomics и наследие ref#
В современном Rust сопоставление по ссылке работает интуитивно благодаря механизму Match Ergonomics (появился в Rust 2018). Если нужно сопоставить данные, не забирая владение над полями (когда тип не реализует Copy), достаточно передать ссылку в сам match:
let point = (String::from("sensor_1"), 42);
// Передаем ссылку &point — компилятор автоматически свяжет name как &String:
match &point {
(name, 0) => println!("Датчик {name} на нулевой отметке"),
(name, val) if *val > 100 => println!("Превышение на датчике {name}: {val}"),
_ => println!("Штатный режим"),
}
// point по-прежнему доступен здесь, так как мы сопоставляли ссылку
Раньше в коде можно было часто встреить ref и ref mut. Почему теперь их редко можно увидеть ?
До появления Match Ergonomics в Rust 2018 компилятор требовал явно указывать режим связывания в каждом шаблоне: (ref name, 0) или ref mut x.
Сегодня явный ref считается устаревшим стилем и в повседневном коде почти не используется. Единственный актуальный кейс для него - частичное перемещение (partial move), когда из структуры одно поле нужно забрать во владение (move), а на второе - взять ссылку:
struct Task {
id: String,
payload: Vec<u8>,
}
// payload перемещаем (move), а id заимствуем по ссылке:
let Task { ref id, payload } = task;Несколько шаблонов через |#
А если несколько веток должны выполнять одинаковый код, их объединяют через символ |:
match status {
"queued" | "pending" => println!("В очереди на обработку"),
"running" | "retrying" => println!("Выполняется"),
_ => println!("Завершено или ошибка"),
}2. @-связывание: пример из утилиты dupe-krill#
Частая задача: проверить, что число или объект попадает в диапазон, и тут же использовать это значение. Без @-связывания пришлось бы делать вложенный if, либо дублировать проверки.
Оператор @ решает проблему элегантно: он проверяет шаблон и одновременно привязывает совпавшее значение к переменной.
Отличный пример - утилита поиска дубликатов файлов dupe-krill (автор Kornel Lesiński). Там длительность сканирования форматируется в красивую строку с помощью сопоставления диапазонов:
// Исходный код из dupe-krill (src/ui.rs):
let nice_duration = match scan_duration.as_secs() {
x @ 0..=5 => format!(
"{:.1}s",
(x * 1_000_000_000 + u64::from(scan_duration.subsec_nanos())) as f64 / 1_000_000_000f64
),
x @ 5..=59 => format!("{x}s"),
x => format!("{}m{}s", x / 60, x % 60),
};Обратите внимание:
- В первой ветке
x @ 0..=5компилятор проверяет, что секунды укладываются в диапазон0..=5, и связывает это число сx, чтобы добавить наносекунды для точного вывода (1.4s). - Во второй ветке
x @ 5..=59число форматируется как целые секунды (42s). - В последней ветке
xпросто забирает оставшиеся значения и делит их на минуты и секунды (2m15s).
Код читается сверху вниз и не требует ни одной вспомогательной переменной.
3. Slice Patterns и Guards: парсер 3D-файлов в normalize_obj#
А теперь рассмотрим Match Guards. Это такие охранные выражения, которые добавляют к ветке произвольное if- условие. Ветка сработает только тогда, когда совпал и шаблон, и условие guard.
Особенно мощно это выглядит в сочетании с сопоставлением срезов (Slice Patterns).
Посмотрим на парсер формата 3D-моделей OFF из проекта normalize_obj (Julian Knodt). Парсер считывает строки файла, разбивает их на слова через .split_whitespace() и сопоставляет полученный срез токенов:
// Фрагмент парсера из normalize_obj (src/off.rs):
match &tokens.as_slice() {
// 1. Игнорируем комментарии: первый токен начинается с '#'
[v, ..] if v.starts_with('#') => continue,
// 2. Читаем заголовок формата:
[off] if off.to_lowercase() == "off" => {
curr = State::Counts;
}
// 3. Фиксированный срез из 3 элементов: координаты вершины (x, y, z)
[x, y, z] if curr == State::Verts => {
let x = x.parse::<f64>()?;
let y = y.parse::<f64>()?;
let z = z.parse::<f64>()?;
verts.push([x, y, z]);
}
// 4. Захват головы и хвоста среза через `@ ..`:
[cnt, r @ ..] if curr == State::Faces => {
let _cnt = cnt.parse::<u32>()?;
let v_idxs = r.iter().map(|vi| vi.parse::<usize>().unwrap()).collect();
faces.push(v_idxs);
}
_ => panic!("Неожиданная строка в файле"),
}Что здесь происходит:
[v, ..]проверяет, что в строке есть хотя бы одно словоv, а всё остальное..игнорирует. Guardif v.starts_with('#')мгновенно отсекает комментарии.[x, y, z]требует, чтобы в срезе было ровно три элемента. Если элементов два или четыре, эта ветка не сопоставится.[cnt, r @ ..]достаёт первый элемент как число гранейcnt, а весь оставшийся срез привязывает к переменнойr.
Весь конечный автомат парсера уместился в один чистый match без ручных проверок длины массива tokens.len() == 3.
4. Примеры: деструктуризация и guards#
В интерактивных слайдах вы можете изучить и запустить примеры с деструктуризацией и условиями:
5. Цепочки if let (let_chains) и цикл while let#
Когда полный match избыточен (например, нужно обработать только один позитивный вариант), на помощь приходит if let.
В современном Rust можно связывать несколько сопоставлений через оператор && в единую цепочку (let chains):
let result: Option<Result<i32, &str>> = Some(Ok(42));
// Разворачиваем вложенные Option и Result в одном условии:
if let Some(res) = result && let Ok(val) = res && val > 10 {
println!("Получено валидное значение: {val}");
}А для работы с очередями, итераторами или каналами Tokio идеально подходит цикл while let:
let mut stack = vec![1, 2, 3];
// Цикл выполняется, пока pop() возвращает Some(item):
while let Some(top) = stack.pop() {
println!("Сняли со стека: {top}");
}Попробуйте интерактивный пример с цепочками условий:
Проверь свои знания!#
Проверьте, насколько хорошо вы освоили тонкости Pattern Matching:


