Конструкция управления потоком match
В Rust есть чрезвычайно мощная конструкция управления потоком под названием
match, которая позволяет сравнить значение с рядом образцов, а затем
выполнить код в зависимости от того, какой образец совпал. Образцы могут
состоять из литеральных значений, имен переменных, подстановочных знаков и
многих других элементов; глава 19 охватывает
все виды образцов и то, что они делают. Сила match заключается в
выразительности образцов и в том, что компилятор подтверждает: обработаны все
возможные случаи.
Представьте выражение match как машину для сортировки монет: монеты
скользят по направляющей, вдоль которой расположены отверстия разного размера,
и каждая монета падает в первое отверстие, в которое она подходит. Так же и
значения проходят через каждый образец в match, и при первом образце, к
которому значение «подходит», значение попадает в связанный с ним блок кода
для использования во время выполнения.
Раз уж речь зашла о монетах, давайте используем их как пример для match! Мы
можем написать функцию, которая принимает неизвестную монету США и, подобно
счетной машине, определяет, что это за монета, и возвращает ее стоимость в
центах, как показано в листинге 6-3.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
match, в котором варианты enum используются как образцыДавайте разберем match в функции value_in_cents. Сначала мы пишем ключевое
слово match, за которым следует выражение; в данном случае это значение
coin. Это очень похоже на условное выражение, используемое с if, но есть
большое отличие: с if условие должно вычисляться в булево значение, а здесь
оно может быть любого типа. Тип coin в этом примере – enum Coin, который
мы определили в первой строке.
Далее идут ветви match. У ветви есть две части: образец и некоторый код. У
первой ветви здесь образцом является значение Coin::Penny, а затем идет
оператор =>, который отделяет образец от кода, который нужно выполнить. В
данном случае код – это просто значение 1. Каждая ветвь отделяется от
следующей запятой.
Когда выражение match выполняется, оно по порядку сравнивает результирующее
значение с образцом каждой ветви. Если образец соответствует значению,
выполняется код, связанный с этим образцом. Если этот образец не соответствует
значению, выполнение продолжается со следующей ветви, почти как в машине для
сортировки монет. У нас может быть столько ветвей, сколько нужно: в листинге
6-3 у нашего match четыре ветви.
Код, связанный с каждой ветвью, является выражением, а результирующее значение
выражения в совпавшей ветви становится значением, которое возвращается для
всего выражения match.
Обычно мы не используем фигурные скобки, если код ветви match короткий, как в
листинге 6-3, где каждая ветвь просто возвращает значение. Если вы хотите
выполнить несколько строк кода в ветви match, нужно использовать фигурные
скобки, и запятая после ветви тогда становится необязательной. Например,
следующий код выводит “Lucky penny!” каждый раз, когда метод вызывается с
Coin::Penny, но все равно возвращает последнее значение блока, 1:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
Образцы, которые связываются со значениями
Еще одна полезная возможность ветвей match заключается в том, что они могут связываться с частями значений, которые соответствуют образцу. Именно так мы можем извлекать значения из вариантов enum.
В качестве примера изменим один из вариантов нашего enum так, чтобы он хранил
данные внутри себя. С 1999 по 2008 год США выпускали 25-центовые монеты с
разными изображениями для каждого из 50 штатов на одной стороне. Никакие
другие монеты не получали изображений штатов, поэтому только у 25-центовых
монет есть это дополнительное значение. Мы можем добавить эту информацию в наш
enum, изменив вариант Quarter так, чтобы он включал значение UsState,
хранящееся внутри него, как сделано в листинге 6-4.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
Coin, в котором вариант Quarter также хранит значение UsStateПредставим, что друг пытается собрать все 50 монет с изображениями штатов. Пока мы сортируем мелочь по типу монеты, мы также будем называть штат, связанный с каждой 25-центовой монетой, чтобы, если такой у друга еще нет, он мог добавить ее в свою коллекцию.
В выражении match для этого кода мы добавляем переменную state в образец,
который соответствует значениям варианта Coin::Quarter. Когда совпадает
Coin::Quarter, переменная state связывается со значением штата этой
25-центовой монеты. Затем мы можем использовать state в коде этой ветви, вот
так:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
Если бы мы вызвали value_in_cents(Coin::Quarter(UsState::Alaska)), значение
coin было бы Coin::Quarter(UsState::Alaska). Когда мы сравниваем это
значение с каждой из ветвей match, ни одна из них не совпадает, пока мы не
дойдем до Coin::Quarter(state). В этот момент привязка state получит
значение UsState::Alaska. Затем мы можем использовать эту привязку в
выражении println!, тем самым извлекая внутреннее значение штата из варианта
enum Coin для Quarter.
Образец match для Option<T>
В предыдущем разделе мы хотели получить внутреннее значение T из случая
Some при использовании Option<T>; мы также можем обрабатывать Option<T> с
помощью match, как мы делали с enum Coin! Вместо сравнения монет мы будем
сравнивать варианты Option<T>, но принцип работы выражения match остается
тем же.
Допустим, мы хотим написать функцию, которая принимает Option<i32> и, если
внутри есть значение, прибавляет к нему 1. Если внутри нет значения, функция
должна вернуть значение None и не пытаться выполнять никаких операций.
Благодаря match эту функцию очень легко написать, и она будет выглядеть как
в листинге 6-5.
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
match для Option<i32>Давайте подробнее рассмотрим первое выполнение plus_one. Когда мы вызываем
plus_one(five), переменная x в теле plus_one будет иметь значение
Some(5). Затем мы сравниваем его с каждой ветвью match:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Значение Some(5) не соответствует образцу None, поэтому мы переходим к
следующей ветви:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Соответствует ли Some(5) образцу Some(i)? Да! У нас тот же вариант.
Переменная i связывается со значением, содержащимся в Some, поэтому i
получает значение 5. Затем выполняется код в ветви match: мы прибавляем 1 к
значению i и создаем новое значение Some, внутри которого находится наш
результат 6.
Теперь рассмотрим второй вызов plus_one в листинге 6-5, где x равно
None. Мы входим в match и сравниваем с первой ветвью:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Совпадает! Значения, к которому можно что-то прибавить, нет, поэтому программа
останавливается и возвращает значение None в правой части =>. Поскольку
первая ветвь совпала, остальные ветви не сравниваются.
Объединение match и enum полезно во многих ситуациях. Вы часто будете видеть
этот шаблон в коде Rust: сопоставить enum с образцами, связать переменную с
данными внутри, а затем выполнить код на их основе. Поначалу это немного
непривычно, но когда вы привыкнете, вам захочется иметь такую возможность во
всех языках. Пользователям она стабильно нравится.
Сопоставления являются исчерпывающими
Есть еще один аспект match, который нужно обсудить: образцы ветвей должны
покрывать все возможности. Рассмотрим эту версию нашей функции plus_one, в
которой есть ошибка и которая не скомпилируется:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Мы не обработали случай None, поэтому этот код приведет к ошибке. К счастью,
Rust умеет такую ошибку обнаруживать. Если мы попробуем скомпилировать этот
код, получим такую ошибку:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust знает, что мы не покрыли все возможные случаи, и даже знает, какой
образец мы забыли! Сопоставления в Rust исчерпывающие: мы должны исчерпать
все до последней возможности, чтобы код был допустимым. Особенно в случае
Option<T>, когда Rust не дает нам забыть явно обработать случай None, он
защищает нас от предположения, что у нас есть значение, когда у нас может быть
null, тем самым делая невозможной ошибку на миллиард долларов, обсуждавшуюся
ранее.
Всеохватывающие образцы и заполнитель _
Используя enum, мы также можем выполнять особые действия для нескольких
конкретных значений, а для всех остальных значений выполнять одно действие по
умолчанию. Представьте, что мы реализуем игру, где, если при броске кости
выпадает 3, ваш игрок не двигается, а вместо этого получает красивую новую
шляпу. Если выпадает 7, ваш игрок теряет красивую шляпу. Для всех остальных
значений игрок перемещается на это число клеток по игровому полю. Вот match,
который реализует эту логику; результат броска кости задан прямо в коде, а
вся остальная логика представлена функциями без тел, потому что их фактическая
реализация выходит за рамки этого примера:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
Для первых двух ветвей образцами являются литеральные значения 3 и 7. Для
последней ветви, которая покрывает все остальные возможные значения, образцом
является переменная, которой мы выбрали имя other. Код, выполняемый для
ветви other, использует эту переменную, передавая ее в функцию
move_player.
Этот код компилируется, хотя мы не перечислили все возможные значения, которые
может иметь u8, потому что последний образец совпадет со всеми значениями,
которые не были явно перечислены. Этот всеохватывающий образец удовлетворяет
требованию, что match должен быть исчерпывающим. Обратите внимание, что
всеохватывающую ветвь нужно ставить последней, потому что образцы проверяются
по порядку. Если бы мы поставили всеохватывающую ветвь раньше, остальные ветви
никогда бы не выполнялись, поэтому Rust предупредит нас, если мы добавим ветви
после всеохватывающей!
В Rust также есть образец, который можно использовать, когда нам нужен
всеохватывающий вариант, но мы не хотим использовать значение в
всеохватывающем образце: _ – это специальный образец, который соответствует
любому значению и не связывается с ним. Так мы говорим Rust, что не будем
использовать значение, поэтому Rust не предупредит нас о неиспользуемой
переменной.
Изменим правила игры: теперь, если выпадает что угодно, кроме 3 или 7, нужно
бросить кость еще раз. Нам больше не нужно использовать всеохватывающее
значение, поэтому мы можем изменить код, чтобы использовать _ вместо
переменной с именем other:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}
Этот пример также удовлетворяет требованию исчерпываемости, потому что в последней ветви мы явно игнорируем все остальные значения; мы ничего не забыли.
Наконец, изменим правила игры еще раз: если выпадает что угодно, кроме 3 или
7, в ваш ход больше ничего не происходит. Мы можем выразить это, используя
единичное значение (пустой кортежный тип, который мы упоминали в разделе
«Кортежный тип») как код, связанный с ветвью _:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}
Здесь мы явно говорим Rust, что не собираемся использовать никакое другое значение, которое не соответствует образцу в более ранней ветви, и не хотим выполнять никакой код в этом случае.
О сопоставлении с образцом есть еще много материала, который мы рассмотрим в
главе 19. А пока мы перейдем к синтаксису
if let, который может быть полезен в ситуациях, когда выражение match
получается немного многословным.