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

> Изучаем как работает сопоставление с образцом Pattern Matching в Rust: exhaustiveness, деструктуризация, @-binding, slice patterns и guards на примерах реальных проектов

- **URL:** https://eagle2000.space/rust/rust-pattern-matching-deep-dive/
- **Date:** 2026-10-05

- **Tags:** rust, dev, pattern-matching, destructuring, match-guards, if-let, while-let

---
## Введение

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

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

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

---

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

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

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


### 💻 Полнота проверок (exhaustiveness) и шаблон _


#### Шаг 1

```rust
// ?compile_fail
enum ConnectionState {
    Disconnected,
    Connecting,
    Connected { ip: String },
}

fn handle_connection(state: ConnectionState) {
    match state {
        ConnectionState::Disconnected => println!("Офлайн"),
        ConnectionState::Connected { ip } => println!("Подключено к {ip}"),
        // Ошибка компиляции: вариант `Connecting` не обработан!
    }
}

fn main() {
    let state = ConnectionState::Connecting;
    handle_connection(state);
}
```

**1. Ошибка компиляции: неполный match**
- Компилятор Rust строго требует исчерпывающей проверки всех вариантов (exhaustiveness).
- Вариант `Connecting` пропущен, поэтому компилятор отклоняет сборку с ошибкой E0004.


#### Шаг 2

```rust
enum ConnectionState {
    Disconnected,
    Connecting,
    Connected { ip: String },
}

fn handle_connection(state: ConnectionState) {
    match state {
        ConnectionState::Disconnected => println!("Офлайн"),
        ConnectionState::Connected { ip } => println!("Подключено к {ip}"),
        // Заглушка `_` перехватывает любые оставшиеся варианты:
        _ => println!("Прочие состояния"),
    }
}

fn main() {
    handle_connection(ConnectionState::Disconnected);
    handle_connection(ConnectionState::Connecting);
    handle_connection(ConnectionState::Connected {
        ip: String::from("192.168.1.1"),
    });
}
```

**2. Исправление заглушкой `_` (wildcard)**
- Шаблон `_` перехватывает любые необработанные варианты, удовлетворяя проверке exhaustiveness.
- Хотя такой вариант компилируется, злоупотреблять им не рекомендуется: при добавлении новых вариантов в enum компилятор больше не предупредит об их пропуске.


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

Сопоставление работает не только в `match`, но и в обычных `let`- выражениях. Можно извлекать нужные поля из глубоко вложенных структур за одно действие:

```rust
struct Address {
    street: String,
    city: String,
}

struct Person {
    name: String,
    address: Address,
}

// Достаём имя и поля вложенного адреса сразу:
let Person { 
    name, 
    address: Address { street, city } 
} = person;
```

Лишние поля можно отбросить с помощью `_` или оператора остатка `..`:

```rust
let Person { name, address: Address { city, .. } } = person;
```

### Заимствование: Match Ergonomics и наследие `ref`

В современном Rust сопоставление по ссылке работает интуитивно благодаря механизму **Match Ergonomics** (появился в Rust 2018). Если нужно сопоставить данные, не забирая владение над полями (когда тип не реализует `Copy`), достаточно передать ссылку в сам `match`:

```rust
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 по-прежнему доступен здесь, так как мы сопоставляли ссылку
```

> [!NOTE]
> **Раньше в коде можно было часто встреить `ref` и `ref mut`. Почему теперь их редко можно увидеть ?**
> 
> До появления [Match Ergonomics в Rust 2018](https://doc.rust-lang.org/edition-guide/rust-2024/match-ergonomics.html) компилятор требовал явно указывать режим связывания в каждом шаблоне: `(ref name, 0)` или `ref mut x`. 
>
> Сегодня явный `ref` считается устаревшим стилем и в повседневном коде почти не используется. Единственный актуальный кейс для него - **частичное перемещение (partial move)**, когда из структуры одно поле нужно забрать во владение (`move`), а на второе - взять ссылку:
> ```rust
> struct Task {
>     id: String,
>     payload: Vec<u8>,
> }
> 
> // payload перемещаем (move), а id заимствуем по ссылке:
> let Task { ref id, payload } = task;
> ```

### Несколько шаблонов через `|`

А если несколько веток должны выполнять одинаковый код, их объединяют через символ `|`:

```rust
match status {
    "queued" | "pending" => println!("В очереди на обработку"),
    "running" | "retrying" => println!("Выполняется"),
    _ => println!("Завершено или ошибка"),
}
```

---

## 2. `@`-связывание: пример из утилиты dupe-krill

Частая задача: проверить, что число или объект попадает в диапазон, и **тут же использовать** это значение. Без `@`-связывания пришлось бы делать вложенный `if`, либо дублировать проверки.

Оператор `@` решает проблему элегантно: он проверяет шаблон и одновременно привязывает совпавшее значение к переменной.

Отличный пример - утилита поиска дубликатов файлов [dupe-krill (автор Kornel Lesiński)](https://github.com/kornelski/dupe-krill/blob/0e813b83c08543d4052523e05ca170bb1d155e91/src/ui.rs#L50). Там длительность сканирования форматируется в красивую строку с помощью сопоставления диапазонов:

```rust
// Исходный код из 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)](https://github.com/JulianKnodt/normalize_obj/blob/a43e1c71a2f2d5c336ba72d840b9293a8496c66c/src/off.rs#L35). Парсер считывает строки файла, разбивает их на слова через `.split_whitespace()` и сопоставляет полученный срез токенов:

```rust
// Фрагмент парсера из 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

```rust
// Шаг 1: Глубокая деструктуризация структур и связывающие шаблоны (@)
struct Address {
    street: String,
    city: String,
}

struct Person {
    name: String,
    address: Address,
}

fn main() {
    let person = Person {
        name: String::from("Алексей"),
        address: Address {
            street: String::from("Ленина"),
            city: String::from("Москва"),
        },
    };

    // 1. Извлечение полей из вложенной структуры за одно действие:
    let Person { name, address: Address { street, city } } = person;
    println!("Имя: {name}, Город: {city}, Улица: {street}");

    // 2. Использование @-связывания для сохранения значения при проверке диапазона:
    let val = 15;
    match val {
        num @ 1..=10 => println!("Число {num} в диапазоне от 1 до 10"),
        num @ 11..=20 => println!("Число {num} в диапазоне от 11 до 20"),
        _ => println!("Другое число"),
    }
}
```

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


#### Шаг 2

```rust
enum Signal {
    Quit,
    Move { x: i32, y: i32 },
    Echo(String),
}

// Шаг 2: Охранные выражения (Match Guards) в перечислениях
fn main() {
    let signal = Signal::Move { x: 20, y: 20 };

    match signal {
        Signal::Quit => println!("Команда на выход"),
        // Match Guard с условием равенства координат:
        Signal::Move { x, y } if x == y => {
            println!("Диагональное перемещение в точку ({x}, {y})");
        }
        Signal::Move { x, y } => {
            println!("Обычное смещение в точку ({x}, {y})");
        }
        Signal::Echo(text) => println!("Текст сигнала: {text}"),
    }
}
```

**2. Охранные выражения (Match Guards)**
- Match Guard (`if CONDITION`) добавляет дополнительное динамическое условие к ветке `match`.
- Компилятор учитывает порядок веток: ветку с guard следует ставить перед общей веткой.


---

## 5. Цепочки `if let` (`let_chains`) и цикл `while let`

Когда полный `match` избыточен (например, нужно обработать только один позитивный вариант), на помощь приходит `if let`.

В современном Rust можно связывать несколько сопоставлений через оператор `&&` в единую цепочку (**let chains**):

```rust
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`**:

```rust
let mut stack = vec![1, 2, 3];

// Цикл выполняется, пока pop() возвращает Some(item):
while let Some(top) = stack.pop() {
    println!("Сняли со стека: {top}");
}
```

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


### 💻 Цепочки if let и цикл while let


#### Шаг 1

```rust
// Шаг 1: Комбинирование условий в цепочках if let (let_chains)
fn main() {
    let result: Option<Result<i32, &str>> = Some(Ok(42));

    // Цепочка if let с комбинированием условий через оператор &&:
    if let Some(res) = result && let Ok(val) = res && val > 10 {
        println!("Успешно извлечено значение больше 10: {val}");
    }
}
```

**1. Цепочки if let (let_chains)**
- В современных версиях Rust можно объединять несколько сопоставлений `let` и булевых выражений через `&&`.
- Это избавляет от необходимости писать несколько вложенных блоков `if let`.


#### Шаг 2

```rust
// Шаг 2: Использование конструкции while let для обхода итераторов
fn main() {
    let numbers = vec![10, 20, 30, 40, 50];
    let mut iter = numbers.into_iter();

    // Цикл while let извлекает элементы, пока iter.next() возвращает Some(val)
    while let Some(val) = iter.next() {
        println!("Обработан элемент из итератора: {val}");
    }

    println!("Итератор завершил работу!");
}
```

**2. Лаконичный цикл while let**
- Конструкция `while let` выполняет цикл до тех пор, пока выражение соответствует заданному шаблону.
- Идеально подходит для работы с итераторами, стек-буферами и очередями данных.


---

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

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


### 📝 Продвинутый Pattern Matching


**1. Какую роль выполняет оператор `@` в шаблонах Pattern Matching?**

- [ ] Сохраняет значение в переменную во время его проверки в диапазоне или шаблоне
- [ ] Принудительно удаляет объект из оперативной памяти
- [ ] Преобразует строку в массив байтов

**2. Что представляет собой охранное выражение (Match Guard) в конструкции `match`?**

- [ ] Специальный деструктор для очистки памяти
- [ ] Дополнительное `if`-условие в ветке `match`, которое должно вернуть `true` для срабатывания данной ветки
- [ ] Защита от перезаписи файлов конфигурации

**3. В чем заключается преимущество цепочек `if let` (let_chains) с оператором `&&`?**

- [ ] Они отключают проверку времён жизни компилятором
- [ ] Они увеличивают точность вычислений чисел с плавающей точкой
- [ ] Они позволяют последовательно извлекать данные из нескольких шаблонов и проверять условия без вложенных блоков

**4. Для какой задачи наиболее удобна конструкция `while let`?**

- [ ] Для выполнения цикла до тех пор, пока выражение совпадает с шаблоном (например, последовательный вызов `iter.next()`)
- [ ] Для создания макросов `macro_rules!`
- [ ] Для вызова C-функций через FFI

**5. Что произойдет при компиляции `match`, если в `enum` добавлен новый вариант, но он не обработан ни в одной ветке и нет шаблона `_`?**

- [ ] Код скомпилируется, но при попадании в этот вариант программа аварийно завершится (`panic!`)
- [ ] Ошибка компиляции: Rust строго требует полноты проверок (exhaustiveness) и не скомпилирует код с необработанным вариантом
- [ ] Компилятор автоматически подставит пустое действие по умолчанию

**6. Как работает механизм Match Ergonomics (начиная с Rust 2018) при передаче ссылки в `match &point`?**

- [ ] Он автоматически клонирует структуру через вызов `.clone()` для каждой ветки
- [ ] Он преобразует ссылку в сырой указатель (`*const T`)
- [ ] Компилятор автоматически выводит режим связывания по ссылке, связывая переменные шаблона как ссылки без ключевого слова `ref`

**7. В какой редкой ситуации в современном Rust всё ещё требуется ключевое слово `ref`?**

- [ ] При частичном перемещении (partial move), когда одно поле структуры перемещается во владение (`move`), а на другое берётся ссылка
- [ ] При каждом объявлении ссылочного поля внутри структуры `struct`
- [ ] Для передачи аргументов в асинхронные задачи Tokio

**8. Что выполняет шаблон `[first, rest @ ..]` при сопоставлении со срезом (Slice Patterns)?**

- [ ] Копирует срез в кольцевой буфер `VecDeque`
- [ ] Извлекает первый элемент в переменную `first`, а весь оставшийся хвост среза связывает с переменной `rest`
- [ ] Сравнивает первый и последний элементы массива на равенство


