Все случаи, где могут быть использованы шаблоны

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

Ветки match

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

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

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

match x {
    None => None,
    Some(i) => Some(i + 1),
}

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

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

Специальный шаблон _ будет соответствовать чему угодно, но он никогда не привязывается к переменной, поэтому он часто используется в последней ветке. Шаблон _ может быть полезен, если вы, например, хотите игнорировать любое не указанное значение. Мы рассмотрим шаблон _ более подробно в разделе "Игнорирование значений в шаблоне позже в этой главе.

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

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

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

Код в листинге 18-1 показывает последовательность проверок нескольких условий, определяющих каким должен быть цвет фона. В данном примере мы создали переменные с предопределёнными значениями, которые в реальной программе могли бы быть получены из пользовательского ввода.

Файл: 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!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

Листинг 18-1: Использование условных конструкций if let, else if, else if let, и else

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

Эта условная структура позволяет поддерживать сложные требования. С жёстко закодированными значениями, которые у нас здесь есть, этот пример напечатает Using purple as the background color.

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

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

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

Аналогично конструкции if let, конструкция условного цикла while let позволяет повторять цикл while до тех пор, пока шаблон продолжает совпадать. Пример в листинге 18-2 демонстрирует цикл while let, который использует вектор в качестве стека и печатает значения вектора в порядке, обратном тому, в котором они были помещены.

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{top}");
    }
}

Листинг 18-2: Использование цикла while let для печати значений до тех пор, пока stack.pop() возвращает Some

В этом примере выводится 3, 2, а затем 1. Метод pop извлекает последний элемент из вектора и возвращает Some(value). Если вектор пуст, то pop возвращает None. Цикл while продолжает выполнение кода в своём блоке, пока pop возвращает Some. Когда pop возвращает None, цикл останавливается. Мы можем использовать while let для удаления каждого элемента из стека.

Цикл for

В цикле for значение, которое следует непосредственно за ключевым словом for , является шаблоном. Например, в for x in y выражение x является шаблоном. В листинге 18-3 показано, как использовать шаблон в цикле for , чтобы деструктурировать или разбить кортеж как часть цикла for .

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

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

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

Код в листинге 18-3 выведет следующее:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [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' и будет напечатана первая строка выходных данных.

Инструкция let

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


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

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

let PATTERN = EXPRESSION;

В инструкциях типа let x = 5; с именем переменной в слоте PATTERN, имя переменной является просто отдельной, простой формой шаблона. Rust сравнивает выражение с шаблоном и присваивает любые имена, которые он находит. Так что в примере let x = 5;, x - это шаблон, который означает "привязать то, что соответствует здесь, переменной x". Поскольку имя x является полностью шаблоном, этот шаблон фактически означает "привязать все к переменной x независимо от значения".

Чтобы более чётко увидеть аспект сопоставления с шаблоном let, рассмотрим листинг 18-4, в котором используется шаблон с let для деструктурирования кортежа.

fn main() {
    let (x, y, z) = (1, 2, 3);
}

Листинг 18-4. Использование шаблона для деструктуризации кортежа и создания трёх переменных одновременно

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

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

fn main() {
    let (x, y) = (1, 2, 3);
}

Листинг 18-5: Неправильное построение шаблона, переменные не соответствуют количеству элементов в кортеже

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

$ 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

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

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

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

fn foo(x: i32) {
    // code goes here
}

fn main() {}

Листинг 18-6: Сигнатура функции использует образцы в параметрах

x это часть шаблона! Как и в случае с let, мы можем сопоставить кортеж в аргументах функции с образцом. Листинг 18-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);
}

Листинг 18-7: Функция с параметрами, которая разрушает кортеж

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

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

На данный момент вы видели несколько способов использования шаблонов, но шаблоны работают не одинаково во всех местах, где их можно использовать. В некоторых местах шаблоны должны быть неопровержимыми; в других обстоятельствах они могут быть опровергнуты. Мы обсудим эти две концепции далее.