Конструкция match
В Rust есть чрезвычайно мощная конструкция управления потоком, именуемая match — она позволяет сравнивать значение с различными шаблонами и исполнять код в зависимости от того, какой из шаблонов совпал. Шаблоны могут состоять из литералов, имён переменных, неопровержимых шаблонов и многого другого; в Главе 19 рассматриваются все различные виды шаблонов и то, что они делают. Сила match заключается в выразительности шаблонов и в том, что компилятор способен проверить, что все возможные случаи обработаны.
Think of a match expression as being like a coin-sorting machine: Coins slide down a track with variously sized holes along it, and each coin falls through the first hole it encounters that it fits into. In the same way, values go through each pattern in a match, and at the first pattern the value “fits,” the value falls into the associated code block to be used during execution.
Кстати, говоря о монетах! Давайте используем их, чтобы показать работу 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 expression that has the variants of the enum as its patternsLet’s break down the match in the value_in_cents function. First, we list the match keyword followed by an expression, which in this case is the value coin. This seems very similar to a conditional expression used with if, but there’s a big difference: With if, the condition needs to evaluate to a Boolean value, but here it can be any type. The type of coin in this example is the Coin enum that we defined on the first line.
Далее идут ветви match. Ветви состоят из двух частей: шаблон и некоторый код. Здесь первая ветвь имеет шаблон, который является значением Coin::Penny, затем идёт оператор =>, который разделяет шаблон и исполняемый код. Код в этом случае — это просто значение 1. Каждая ветвь отделяется от последующей при помощи запятой.
When the match expression executes, it compares the resultant value against the pattern of each arm, in order. If a pattern matches the value, the code associated with that pattern is executed. If that pattern doesn’t match the value, execution continues to the next arm, much as in a coin-sorting machine. We can have as many arms as we need: In Listing 6-3, our match has four arms.
Код, связанный с каждой ветвью, является выражением, а результирующее значение выражения в соответствующем ответвлении — это значение, которое возвращается из всего выражения match.
We don’t typically use curly brackets if the match arm code is short, as it is in Listing 6-3 where each arm just returns a value. If you want to run multiple lines of code in a match arm, you must use curly brackets, and the comma following the arm is then optional. For example, the following code prints “Lucky penny!” every time the method is called with a Coin::Penny, but it still returns the last value of the block, 1:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Счастливый пенни!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
Связывание со значениями с помощью шаблонов
У ветвей выражения match есть ещё одна полезная особенность: они могут привязываться к частям тех значений, которые совпали с шаблоном. Благодаря этому возможно извлекать значения из вариантов перечисления.
В качестве демонстрации, давайте изменим один из вариантов перечисления так, чтобы он хранил в себе данные. С 1999 по 2008, Соединённые Штаты чеканили особенные четвертаки: с уникальными дизайнами для каждого из 50 штатов. Ни одна другая монета не получила дизайна, особенного для отдельных штатов, только четверть доллара имела эту особенность. Мы можем добавить эту информацию в наш enum путём изменения варианта Quarter и включения в него значения UsState, как сделано в Листинге 6-4.
#[derive(Debug)] // нужно, чтобы мы могли легко посмотреть конкретный штат
enum UsState {
Alabama,
Alaska,
// --код сокращён--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
Coin enum in which the Quarter variant also holds a UsState valueПредставьте, что ваш друг пытается собрать четвертаки всех 50 штатов. Сортируя монеты по типу, мы также будем сообщать название штата, к которому относится каждый четвертак, чтобы, если у нашего друга нет такой монеты, он мог добавить её в свою коллекцию.
In the match expression for this code, we add a variable called state to the pattern that matches values of the variant Coin::Quarter. When a Coin::Quarter matches, the state variable will bind to the value of that quarter’s state. Then, we can use state in the code for that arm, like so:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --код сокращён--
}
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:?}!");
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
Если мы сделаем вызов функции value_in_cents(Coin::Quarter(UsState::Alaska)), то coin будет иметь значение Coin::Quarter(UsState::Alaska). Когда мы будем сравнивать это значение с шаблонами каждой из ветвей, ни одному из них оно не будет отвечать, пока мы не достигнем Coin::Quarter(state). В этот момент state свяжется со значением UsState::Alaska. Затем мы сможем использовать эту переменную в выражении println!, получив таким образом внутреннее значение варианта Quarter перечисления Coin.
The Option<T> match Pattern
В предыдущем разделе мы хотели получить внутреннее значение T для случая Some при использовании Option<T>. Мы можем обработать тип Option<T>, используя match, как уже делали с перечислением 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 expression on an Option<i32>Let’s examine the first execution of plus_one in more detail. When we call plus_one(five), the variable x in the body of plus_one will have the value Some(5). We then compare that against each match arm:
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. Затем исполняется код, связанный с данной ветвью: мы добавляем 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 и перечислений полезно во многих ситуациях. Вы часто будете видеть подобную комбинацию в коде на Rust: перебор вариантов перечисления конструкцией match, связывание переменной к данным внутри значения, исполнение код на основе извлечённых данных. Сначала это может показаться немного сложным, но как только вы привыкнете, то захотите, чтобы такая возможность была бы во всех языках. match никого не оставляет равнодушным.
Ветви match охватывают все возможные случаи
There’s one other aspect of match we need to discuss: The arms’ patterns must cover all possibilities. Consider this version of our plus_one function, which has a bug and won’t compile:
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 knows that we didn’t cover every possible case and even knows which pattern we forgot! Matches in Rust are exhaustive: We must exhaust every last possibility in order for the code to be valid. Especially in the case of Option<T>, when Rust prevents us from forgetting to explicitly handle the None case, it protects us from assuming that we have a value when we might have null, thus making the billion-dollar mistake discussed earlier impossible.
Универсальные шаблоны и заполнитель _
Using enums, we can also take special actions for a few particular values, but for all other values take one default action. Imagine we’re implementing a game where, if you roll a 3 on a dice roll, your player doesn’t move but instead gets a fancy new hat. If you roll a 7, your player loses a fancy hat. For all other values, your player moves that number of spaces on the game board. Here’s a match that implements that logic, with the result of the dice roll hardcoded rather than a random value, and all other logic represented by functions without bodies because actually implementing them is out of scope for this example:
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.
This code compiles, even though we haven’t listed all the possible values a u8 can have, because the last pattern will match all values not specifically listed. This catch-all pattern meets the requirement that match must be exhaustive. Note that we have to put the catch-all arm last because the patterns are evaluated in order. If we had put the catch-all arm earlier, the other arms would never run, so Rust will warn us if we add arms after a catch-all!
В Rust также есть шаблон, который можно использовать, когда нам не нужно значение, с которым связывается универсальный шаблон: _. Он является специальным шаблоном, который соответствует любому значению и не привязывается к нему. Его использование говорит Rust, что мы не собираемся использовать это значение, поэтому Rust не будет предупреждать нас о неиспользуемой переменной.
Let’s change the rules of the game: Now, if you roll anything other than a 3 or a 7, you must roll again. We no longer need to use the catch-all value, so we can change our code to use _ instead of the variable named 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. Мы можем реализовать это, используя unit (тип пустого кортежа, который мы упоминали в разделе “Тип кортежа”) на месте кода, который соответствует ветви _:
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 слишком многословно.