Конструкция управления потоком 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, где варианты перечисления используются в качестве образцовРазберём match в функции value_in_cents. Сначала указываем ключевое слово match, за которым следует выражение — в данном случае значение coin. Это похоже на условное выражение с if, но есть важное отличие: в if условие должно оцениваться в логическое значение, а здесь может быть любой тип. Тип coin в этом примере — перечисление Coin, которое мы определили в первой строке.
Далее идут ветви match. Каждая ветвь состоит из двух частей: образца и кода. Первая ветвь имеет образец — значение Coin::Penny — и оператор =>, который разделяет образец и код для выполнения. Код в этом случае — просто значение 1. Каждую ветвь отделяет от следующей запятая.
При выполнении выражения match оно сравнивает полученное значение с образцом каждой ветви по порядку. Если образец совпадает со значением, выполняется код, связанный с этим образцом. Если образец не совпадает, выполнение переходит к следующей ветви, как в монетоприёмнике. Ветвей может быть сколько угодно: в Листинге 6-3 у нашего 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 — они могут привязывать к частям значений, которые совпадают с образцом. Именно так мы извлекаем значения из вариантов перечисления.
Например, изменим один из вариантов нашего перечисления, чтобы он содержал данные. С 1999 по 2008 год в США чеканили четвертаки с разными дизайнами 50 штатов на одной стороне. Никакие другие монеты не имели таких дизайнов, поэтому только четвертаки имеют это дополнительное значение. Мы можем добавить эту информацию в наше 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 штатных четвертаков. Пока мы сортируем мелочь по типу монет, мы также называем название штата, связанного с каждым четвертаком, чтобы, если его нет у друга, он мог добавить его в коллекцию.
В выражении match для этого кода мы добавляем переменную с именем state в образец, который совпадает со значениями варианта Coin::Quarter. Когда Coin::Quarter совпадает, переменная state привяжется к значению штата этого четвертака. Затем мы можем использовать 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!, таким образом получая внутреннее значение штата из варианта 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); }
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 привязывается к значению, contained в 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 и перечислений полезно во многих ситуациях. Вы увидите этот шаблон очень часто в коде Rust: сопоставление с перечислением, привязка переменной к данным внутри и затем выполнение кода на основе этого. Сначала это немного сложно, но как только вы привыкнете, вы захотите иметь это во всех языках. Это последовательно любимый пользователями шаблон.
Соответствия исчерпывающи
Есть ещё один аспект 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/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:572:1
::: /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:576: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, тем самым делая невозможным миллиардную ошибку, обсуждавшуюся ранее.
Универсальные образцы и заполнитель _
Используя перечисления, мы также можем предпринимать специальные действия для нескольких конкретных значений, но для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы реализуем игру, в которой, если при броске кубика выпадает 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.
Как соответствия взаимодействуют с владением
Если перечисление содержит некопируемые данные, такие как String, то следует быть осторожным с тем, перемещает или заимствует match эти данные. Например, эта программа с использованием Option<String> скомпилируется:
Но если мы заменим заполнитель в Some(_) на имя переменной, например Some(s), то программа НЕ скомпилируется:
opt — это обычное перечисление — его тип Option<String>, а не ссылка, такая как &Option<String>. Поэтому match по opt переместит неигнорируемые поля, такие как s. Обратите внимание, как opt теряет разрешения на чтение и владение раньше во второй программе по сравнению с первой. После выражения match данные внутри opt были перемещены, поэтому чтение opt в println недопустимо.
Если мы хотим заглянуть в opt без перемещения его содержимого, идиоматичным решением является сопоставление по ссылке:
Rust «протолкнёт» ссылку из внешнего перечисления, &Option<String>, во внутреннее поле, &String. Поэтому s имеет тип &String, и opt можно использовать после match. Чтобы лучше понять механизм «проталкивания», см. раздел о режимах привязки в Справочнике Rust.