Конструкция 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
в функции value_in_cents
. Сначала пишется ключевое слово match
, затем следует выражение, которое в данном случае является значением coin
. Это выглядит очень похожим на условное выражение при if
, но есть важное отличие: в if
выражение должно возвращать логическое значение, а здесь это может быть любой тип. Тип coin
в этом примере — перечисление типа Coin
, объявленное в первой строке.
Далее идут ветви match
. Ветви состоят из двух частей: шаблон и некоторый код. Здесь первая ветвь имеет шаблон, который является значением Coin::Penny
, затем идёт оператор =>
, который разделяет шаблон и исполняемый код. Код в этом случае — это просто значение 1
. Каждая ветвь отделяется от последующей при помощи запятой.
Когда исполняется выражение match
, оно последовательно сравнивает полученное значение с шаблоном каждой ветви. Если значение отвечает шаблону, то код, связанный с этим шаблоном, исполняется. Если значение не отвечает этому шаблону, то происходит попытка сопоставить значение со следующим шаблоном: по аналогии с автоматом по сортировке монет. У нас может быть столько ветвей, сколько нужно: в Листинге 6-3 наш match
состоит из четырёх ветвей.
Код, связанный с каждой ветвью, является выражением, а результирующее значение выражения в соответствующем ответвлении — это значение, которое возвращается из всего выражения match
.
Обычно фигурные скобки не используются, если код совпадающей ветви невелик (как в Листинге 6-3, где каждая ветвь просто возвращает значение). Если вы хотите выполнить несколько строк кода в одной ветви, вы должны использовать фигурные скобки; запятая после этой ветви необязательна. Например, следующий код печатает "Счастливый пенни!" каждый раз, когда метод вызывается с Coin::Penny
, но при этом он возвращает последнее значение блока — 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() {}
Представьте, что ваш друг пытается собрать четвертаки всех 50 штатов. Сортируя монеты по типу, мы также будем сообщать название штата, к которому относится каждый четвертак, чтобы, если у нашего друга нет такой монеты, он мог добавить её в свою коллекцию.
В выражении match
мы добавляем переменную с именем state
в шаблон, который соответствует значениям варианта Coin::Quarter
. Когда Coin::Quarter
сопоставится с шаблоном, переменная state
станет связана со значением штата этого четвертака. Затем мы сможем использовать state
в коде этой ветки; вот так:
#[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
.
Сопоставление с вариантами Option<T>
В предыдущем разделе мы хотели получить внутреннее значение 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); }
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
охватывают все возможные случаи
Есть ещё одна особенность 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
--> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/option.rs:571:1
|
571 | pub enum Option<T> {
| ^^^^^^^^^^^^^^^^^^
...
575 | None,
| ---- 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 значения, и оберегая нас от опасности совершить ошибку на миллиард долларов, о которой говорилось ранее.
Универсальные шаблоны и заполнитель _
Используя перечисления, мы также можем выполнять специальные действия для нескольких определённых значений, а для всех остальных значений выполнять одно общее действие по умолчанию. Представьте, что мы реализуем игру, в которой при выпадении 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
.
Этот код компилируется, даже хотя мы не перечислили все возможные случаи: всё потому что последний шаблон будет соответствовать всем значениям, не указанным в конкретном списке. Этот универсальный шаблон удовлетворяет требованию, что сопоставление должно быть исчерпывающим. Обратите внимание, что мы должны поместить ветвь с универсальным шаблоном последней, потому что сопоставление с шаблонами происходит по порядку. 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. Мы можем реализовать это, используя 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
слишком многословно.