↓ Перейти к основному содержимому
  1. Rust/

Rust: тонкости Pattern Matching - деструктуризация, bindings и guards

·1870 слов·9 минут· loading · loading · · ·Rust-middle
О Rust - Эта статья часть цикла.
Статей прочитано 0/41
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
41 Rust: тонкости Pattern Matching - деструктуризация, bindings и guards (текущая)
Не прочитана

Введение
#

Паттерн-матчинг в Rust часто воспринимают как аналог оператора switch/case из других языков. На деле это один из самых полезных инструментов в языке. Для нас авторы языка позаботились добавить выражения match, if let и let которые умеют разбирать структуры прямо на лету, проверять условия без вложенных if и отсекать невалидные состояния ещё на этапе компиляции.

В прошлых статьях я разбирал работу с enum rust-enums и управляющими конструкциями (типичные if/else) rust-conditionals.

Сегодня давайте отойдём от базовых примеров и разберём, как сопоставление с образцом (Pattern Matching) работает на практике: от @-биндингов в CLI-утилитах до разбора срезов и конечных автоматов в реальных open-source проектах. Я постарался добавить побольше наглядных примеров с кодом.


1. Принцип полноты и деструктуризация
#

Главное требование компилятора к любому match — полнота проверок (exhaustiveness). Если тип может принять три состояния, вы обязаны явно обработать все три, либо поставить такую заглушку _ которую можно воспринимать, как «ну и всё остальное не попадающее под прошлые варианты» (аналог default).

В слайдах ниже на первом шаге показано, как компилятор пресекает попытку оставить вариант необработанным, а на втором - как сопоставление закрывается с помощью заглушки _ (хотя злоупотреблять ей не стоит, чтобы не пропустить добавления новых состояний в будущем):

Полнота проверок (exhaustiveness) и шаблон _
Шаг 1/2
 1// ?compile_fail
 2enum ConnectionState {
 3    Disconnected,
 4    Connecting,
 5    Connected { ip: String },
 6}
 7
 8fn handle_connection(state: ConnectionState) {
 9    match state {
10        ConnectionState::Disconnected => println!("Офлайн"),
11        ConnectionState::Connected { ip } => println!("Подключено к {ip}"),
12        // Ошибка компиляции: вариант `Connecting` не обработан!
13    }
14}
15
16fn main() {
17    let state = ConnectionState::Connecting;
18    handle_connection(state);
19}
20

1. Ошибка компиляции: неполный match

  • Компилятор Rust строго требует исчерпывающей проверки всех вариантов (exhaustiveness).
  • Вариант Connecting пропущен, поэтому компилятор отклоняет сборку с ошибкой E0004.

Разбор структур и кортежей на лету
#

Сопоставление работает не только в 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!("Неожиданная строка в файле"),
}

Что здесь происходит:

  1. [v, ..] проверяет, что в строке есть хотя бы одно слово v, а всё остальное .. игнорирует. Guard if v.starts_with('#') мгновенно отсекает комментарии.
  2. [x, y, z] требует, чтобы в срезе было ровно три элемента. Если элементов два или четыре, эта ветка не сопоставится.
  3. [cnt, r @ ..] достаёт первый элемент как число граней cnt, а весь оставшийся срез привязывает к переменной r.

Весь конечный автомат парсера уместился в один чистый match без ручных проверок длины массива tokens.len() == 3.


4. Примеры: деструктуризация и guards
#

В интерактивных слайдах вы можете изучить и запустить примеры с деструктуризацией и условиями:

Деструктуризация, @-связывание и Match Guards
Шаг 1/2
 1// Шаг 1: Глубокая деструктуризация структур и связывающие шаблоны (@)
 2struct Address {
 3    street: String,
 4    city: String,
 5}
 6
 7struct Person {
 8    name: String,
 9    address: Address,
10}
11
12fn main() {
13    let person = Person {
14        name: String::from("Алексей"),
15        address: Address {
16            street: String::from("Ленина"),
17            city: String::from("Москва"),
18        },
19    };
20
21    // 1. Извлечение полей из вложенной структуры за одно действие:
22    let Person { name, address: Address { street, city } } = person;
23    println!("Имя: {name}, Город: {city}, Улица: {street}");
24
25    // 2. Использование @-связывания для сохранения значения при проверке диапазона:
26    let val = 15;
27    match val {
28        num @ 1..=10 => println!("Число {num} в диапазоне от 1 до 10"),
29        num @ 11..=20 => println!("Число {num} в диапазоне от 11 до 20"),
30        _ => println!("Другое число"),
31    }
32}
33

1. Деструктуризация и @-связывание

  • Шаблоны позволяют разбирать вложенные структуры и перечисления за один шаг без временных переменных.
  • Оператор @ сохраняет привязанное значение в переменную одновременно с проверкой диапазона или шаблона.

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

Попробуйте интерактивный пример с цепочками условий:

Цепочки if let и цикл while let
Шаг 1/2
 1// Шаг 1: Комбинирование условий в цепочках if let (let_chains)
 2fn main() {
 3    let result: Option<Result<i32, &str>> = Some(Ok(42));
 4
 5    // Цепочка if let с комбинированием условий через оператор &&:
 6    if let Some(res) = result && let Ok(val) = res && val > 10 {
 7        println!("Успешно извлечено значение больше 10: {val}");
 8    }
 9}
10

1. Цепочки if let (let_chains)

  • В современных версиях Rust можно объединять несколько сопоставлений let и булевых выражений через &&.
  • Это избавляет от необходимости писать несколько вложенных блоков if let.

Проверь свои знания!
#

Проверьте, насколько хорошо вы освоили тонкости Pattern Matching:

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
▶ Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/41
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
41 Rust: тонкости Pattern Matching - деструктуризация, bindings и guards (текущая)
Не прочитана

Связанные статьи