Все места для использования шаблонов

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

Ветки match

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

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

Одно из требований к выражениям 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, {}, as the background", color);
    } 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

В главе 3 мы упоминали, что цикл for является самой распространённой конструкцией цикла в коде Rust, но мы ещё не обсуждали шаблон, который использует 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!("{} is at index {}", value, 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 чтобы адаптировать итератор для создания значения и индекса этого значения в итераторе, помещённом в кортеж. Первый вызов 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 `(_, _)`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns`

To learn more, run the command again with --verbose.

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

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

Параметры функции также могут быть образцами. Код в Листинге 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: Функция с параметрами, которая разрушает кортеж

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

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

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