Шаблоны и сопоставления

Шаблоны - это специальный синтаксис в Rust для сопоставления со структурой типов, как сложных, так и простых. Использование шаблонов в сочетании с выражениями match и другими конструкциями даёт вам больший контроль над потоком управления программы.

Шаблон состоит из некоторой комбинации следующего:

  • Литералы
  • Деструктурированные массивы, перечисления, структуры или кортежи
  • Переменные
  • Специальные символы
  • Заполнители

Чтобы использовать шаблон, мы сравниваем его с некоторым значением. Если шаблон соответствует значению, мы используем части значения в нашем дальнейшем коде.

Все места, где можно использовать паттерны

Паттерны встречаются в нескольких местах в Rust, и вы уже много раз использовали их, не осознавая этого! В этом разделе обсуждаются все места, где допустимо использование паттернов.

Ветки match

Как обсуждалось в главе 6, мы используем паттерны в ветках выражений match. Формально выражения match определяются как ключевое слово match, значение, с которым происходит сопоставление, и одна или несколько веток match, которые состоят из паттерна и выражения, выполняемого в случае, если значение соответствует паттерну этой ветки, как показано ниже:

#![allow(unused)]
fn main() {
match ЗНАЧЕНИЕ {
    ПАТТЕРН => ВЫРАЖЕНИЕ,
    ПАТТЕРН => ВЫРАЖЕНИЕ,
    ПАТТЕРН => ВЫРАЖЕНИЕ,
}
}

Например, рассмотрим выражение match из листинга 6-5, которое сопоставляется со значением Option<i32> в переменной x:

#![allow(unused)]
fn main() {
match x {
    None => None,
    Some(i) => Some(i + 1),
}
}

Паттернами в этом выражении match являются None и Some(i) слева от каждой стрелки.

Одним из требований к выражениям match является то, что они должны быть исчерпывающими (exhaustive), в том смысле, что все возможные варианты значения в выражении match должны быть обработаны. Один из способов обеспечить это — иметь универсальный паттерн для последней ветки: например, имя переменной, которое сопоставляется с любым значением, никогда не может завершиться неудачей и, таким образом, покрывает все оставшиеся случаи.

Конкретный паттерн _ будет соответствовать чему угодно, но он никогда не привязывается к переменной, поэтому его часто используют в последней ветке match. Паттерн _ может быть полезен, когда вы хотите проигнорировать любое значение, не указанное ранее. Мы подробнее рассмотрим паттерн _ далее в этой главе, в разделе «Игнорирование значений в паттерне».

Операторы let

До этой главы мы явно обсуждали использование паттернов только с match и if let, но на самом деле мы также использовали паттерны и в других местах, включая операторы let. Например, рассмотрим простое присваивание переменной с помощью let:

#![allow(unused)]
fn main() {
let x = 5;
}

Каждый раз, когда вы использовали оператор let таким образом, вы использовали паттерны, хотя, возможно, не осознавали этого! Более формально оператор let выглядит так:

#![allow(unused)]
fn main() {
let ПАТТЕРН = ВЫРАЖЕНИЕ;
}

В таких инструкциях, как let x = 5;, где в позиции ПАТТЕРНА находится имя переменной, это имя переменной представляет собой просто особо простую форму паттерна. Rust сравнивает выражение с паттерном и присваивает имена, которые он находит. Таким образом, в примере let x = 5; x — это паттерн, который означает «связать то, что совпадает здесь, с переменной x». Поскольку имя x является целым паттерном, этот паттерн фактически означает «связать всё с переменной x, независимо от значения».

Чтобы более ясно увидеть аспект сопоставления с паттерном в let, рассмотрим листинг 19-1, который использует паттерн с let для деструктуризации кортежа.

#![allow(unused)]
fn main() {
let (x, y, z) = (1, 2, 3);
}

Листинг 19-1: Использование паттерна для деструктуризации кортежа и создания трёх переменных одновременно

Здесь мы сопоставляем кортеж с паттерном. Rust сравнивает значение (1, 2, 3) с паттерном (x, y, z) и видит, что значение соответствует паттерну — то есть видит, что количество элементов в обоих случаях одинаково, — поэтому Rust связывает 1 с x, 2 с y и 3 с z. Вы можете думать об этом паттерне кортежа как о трёх отдельных паттернах переменных, вложенных в него.

Если количество элементов в паттерне не совпадает с количеством элементов в кортеже, общие типы не совпадут, и мы получим ошибку компилятора. Например, в листинге 19-2 показана попытка деструктуризировать кортеж с тремя элементами в две переменные, которая не сработает.

#![allow(unused)]
fn main() {
// [Этот код не компилируется!]
let (x, y) = (1, 2, 3);
}

Листинг 19-2: Некорректное построение паттерна, переменные которого не соответствуют количеству элементов в кортеже

Попытка скомпилировать этот код приводит к ошибке типа:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Чтобы исправить ошибку, мы можем проигнорировать одно или несколько значений в кортеже с помощью _ или .., как вы увидите в разделе «Игнорирование значений в паттерне». Если проблема в том, что у нас слишком много переменных в паттерне, решение — привести типы в соответствие, удалив переменные так, чтобы количество переменных равнялось количеству элементов в кортеже.

Условные выражения if let

В главе 6 мы обсуждали, как использовать выражения if let в основном как более краткий способ написания эквивалента match, который обрабатывает только один случай. При необходимости, if let может иметь соответствующий else, содержащий код для выполнения, если паттерн в if let не совпадает.

В листинге 19-3 показано, что также можно комбинировать выражения if let, else if и else if let. Это даёт нам больше гибкости, чем выражение match, в котором мы можем выразить только одно значение для сравнения с паттернами. Кроме того, Rust не требует, чтобы условия в серии из if let, else if и else if let были связаны друг с другом.

Код в листинге 19-3 определяет, какой цвет установить для фона на основе серии проверок нескольких условий. Для этого примера мы создали переменные с жёстко заданными значениями, которые реальная программа могла бы получить из пользовательского ввода.

Файл: src/main.rs

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Используем ваш любимый цвет {color} как цвет фона");
    } else if is_tuesday {
        println!("Вторник — день зелёного цвета!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Используем фиолетовый как цвет фона");
        } else {
            println!("Используем оранжевый как цвет фона");
        }
    } else {
        println!("Используем синий как цвет фона");
    }
}

Листинг 19-3: Комбинирование if let, else if, else if let и else

Если пользователь указывает любимый цвет, этот цвет используется как фон. Если любимый цвет не указан и сегодня вторник, цвет фона — зелёный. В противном случае, если пользователь указывает свой возраст в виде строки и мы можем успешно распарсить его как число, цвет будет фиолетовым или оранжевым в зависимости от значения числа. Если ни одно из этих условий не применимо, цвет фона — синий.

Такая условная структура позволяет нам поддерживать сложные требования. С имеющимися у нас жёстко заданными значениями этот пример напечатает Используем фиолетовый как цвет фона.

Вы можете видеть, что if let также может вводить новые переменные, которые затеняют существующие переменные, точно так же, как это могут делать ветки match: строка if let Ok(age) = age вводит новую переменную age, которая содержит значение внутри варианта Ok, затеняя существующую переменную age. Это означает, что нам нужно поместить условие if age > 30 внутри этого блока: мы не можем объединить эти два условия в if let Ok(age) = age && age > 30. Новая переменная age, которую мы хотим сравнить с 30, не действительна до начала новой области видимости с фигурной скобкой.

Недостатком использования выражений if let является то, что компилятор не проверяет исчерпываемость (exhaustiveness), в отличие от выражений match. Если бы мы опустили последний блок else и тем самым пропустили обработку некоторых случаев, компилятор не предупредил бы нас о возможной логической ошибке.

Условные циклы while let

По структуре похожий на if let, условный цикл while let позволяет циклу while выполняться до тех пор, пока паттерн продолжает совпадать. В листинге 19-4 мы показываем цикл while let, который ожидает сообщения, отправленные между потоками, но в данном случае проверяет Result, а не Option.

#![allow(unused)]
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
    for val in [1, 2, 3] {
        tx.send(val).unwrap();
    }
});

while let Ok(value) = rx.recv() {
    println!("{value}");
}
}

Листинг 19-4: Использование цикла while let для вывода значений, пока rx.recv() возвращает Ok

Этот пример выводит 1, 2, а затем 3. Метод recv извлекает первое сообщение с приёмной стороны канала и возвращает Ok(value). Когда мы впервые увидели recv в главе 16, мы либо напрямую разворачивали ошибку, либо работали с ним как с итератором с помощью цикла for. Однако, как показывает листинг 19-4, мы также можем использовать while let, потому что метод recv возвращает Ok при каждом поступлении сообщения (пока существует отправитель), а затем возвращает Err, когда сторона отправителя отключается.

Циклы for

В цикле for значение, которое следует непосредственно за ключевым словом for, является паттерном. Например, в конструкции for x in y элемент x — это паттерн. В листинге 19-5 показано, как использовать паттерн в цикле for для деструктуризации (или разбора) кортежа как части цикла.

#![allow(unused)]
fn main() {
let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{value} is at index {index}");
}
}

Листинг 19-5: Использование паттерна в цикле for для деструктуризации кортежа

Код из листинга 19-5 выведет следующее:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

Мы адаптируем итератор с помощью метода enumerate, чтобы он генерировал значение и индекс для этого значения, помещённые в кортеж. Первое сгенерированное значение — это кортеж (0, 'a'). Когда это значение сопоставляется с паттерном (index, value), index будет равен 0, а value будет 'a', что приводит к выводу первой строки результата.

Параметры функций

Параметры функций также могут быть паттернами. Код в листинге 19-6, который объявляет функцию с именем foo, принимающую один параметр x типа i32, уже должен выглядеть знакомо.

#![allow(unused)]
fn main() {
fn foo(x: i32) {
    // код функции
}
}

Листинг 19-6: Сигнатура функции, использующая паттерны в параметрах

Часть x — это паттерн! Как мы делали с let, мы можем сопоставить кортеж в аргументах функции с паттерном. Листинг 19-7 разделяет значения кортежа при передаче его в функцию.

Файл: src/main.rs

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Листинг 19-7: Функция с параметрами, которые деструктурируют кортеж

Этот код выводит Current location: (3, 5). Значения &(3, 5) соответствуют паттерну &(x, y), поэтому x получает значение 3, а y — значение 5.

Мы также можем использовать паттерны в списках параметров замыканий таким же образом, как и в списках параметров функций, поскольку замыкания похожи на функции, как обсуждалось в главе 13.

На этом этапе вы увидели несколько способов использования паттернов, но паттерны работают не одинаково во всех местах, где мы можем их использовать. В некоторых местах паттерны должны быть неопровержимыми (irrefutable); в других обстоятельствах они могут быть опровержимыми (refutable). Далее мы обсудим эти два понятия.