Конструкция управления потоком `match`

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Конструкцию управления потоком "match". Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этой лабораторной работе мы исследуем мощную конструкцию управления потоком match в Rust, которая позволяет выполнять сопоставление шаблонов и выполнение кода на основе сопоставленного шаблона.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100399{{"Конструкция управления потоком `match`"}} rust/integer_types -.-> lab-100399{{"Конструкция управления потоком `match`"}} rust/function_syntax -.-> lab-100399{{"Конструкция управления потоком `match`"}} rust/expressions_statements -.-> lab-100399{{"Конструкция управления потоком `match`"}} rust/method_syntax -.-> lab-100399{{"Конструкция управления потоком `match`"}} rust/operator_overloading -.-> lab-100399{{"Конструкция управления потоком `match`"}} end

Конструкция управления потоком "match"

В Rust есть极其强大的控制流结构,称为match,它允许您将一个值与一系列模式进行比较,然后根据匹配的模式执行代码。模式可以由字面量值、变量名、通配符和许多其他内容组成;第18章涵盖了所有不同类型的模式及其作用。match的强大之处在于模式的表现力以及编译器确认所有可能情况都得到处理这一事实。

可以将match表达式想象成一台硬币分类机:硬币沿着一条有各种大小孔洞的轨道下滑,每枚硬币会落入它遇到的第一个能容纳它的孔洞。同样,值会在match中遍历每个模式,并且在值“适合”的第一个模式处,该值会落入相关的代码块中,以便在执行期间使用。

说到硬币,让我们以它们为例来使用match!我们可以编写一个函数,该函数接受一枚未知的美国硬币,并以类似于计数机的方式确定它是哪种硬币,并返回其以美分为单位的值,如清单6-3所示。

1 enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  2 match coin {
      3 Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

清单6-3:一个枚举和一个match表达式,该表达式将枚举的变体用作其模式

让我们分析一下value_in_cents函数中的match。首先,我们列出match关键字,后面跟着一个表达式,在这种情况下是值coin [2]。这看起来与用于if的表达式非常相似,但有一个很大的区别:对于if,表达式需要返回一个布尔值,但在这里它可以返回任何类型。在这个例子中,coin的类型是我们在[1]处定义的Coin枚举。

接下来是match分支。一个分支有两部分:一个模式和一些代码。这里的第一个分支有一个模式,即值Coin::Penny,然后是分隔模式和要运行的代码的=>运算符 [3]。在这种情况下,代码只是值1。每个分支用逗号与下一个分支隔开。

match表达式执行时,它会按顺序将结果值与每个分支的模式进行比较。如果一个模式与值匹配,则执行与该模式相关联的代码。如果该模式与值不匹配,则执行继续到下一个分支,就像在硬币分类机中一样。我们可以根据需要有任意多个分支:在清单6-3中,我们的match有四个分支。

与每个分支相关联的代码是一个表达式,并且匹配分支中表达式的结果值就是整个match表达式返回的值。

如果match分支代码很短,我们通常不使用花括号,就像在清单6-3中每个分支只返回一个值那样。如果您想在match分支中运行多行代码,则必须使用花括号,然后分支后面的逗号是可选的。例如,以下代码在每次使用Coin::Penny调用该方法时都会打印“幸运便士!”,但仍然返回块的最后一个值,即1

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Паттерны, которые связываются с значениями

Еще один полезный признак ветвей match заключается в том, что они могут связываться с частями значений, которые соответствуют шаблону. Именно так мы можем извлекать значения из вариантов перечисления.

В качестве примера давайте изменим один из вариантов нашего перечисления так, чтобы он содержал внутри себя данные. С 1999 по 2008 годы в Соединенных Штатах выпускались 25-центные монеты с разными дизайнами для каждой из 50 штатов с одной стороны. Другие монеты не имели государственных дизайнов, поэтому только 25-центные монеты имеют это дополнительное значение. Мы можем добавить эту информацию в наше enum, изменив вариант Quarter так, чтобы он включал значение UsState, хранящееся внутри него, что мы сделали в Listing 6-4.

#[derive(Debug)] // чтобы мы могли проверить состояние через минуту
enum UsState {
    Alabama,
    Alaska,
    --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

Listing 6-4: Перечисление Coin, в котором вариант Quarter также хранит значение UsState

Представьте, что друг пытается собрать все 50 государственных 25-центных монет. Во время сортировки наших мелких монет по типу монеты мы также вызовем название штата, связанного с каждой 25-центной монетой, чтобы если это монета, которой у друга нет, они могли добавить ее в свою коллекцию.

В выражении match для этого кода мы добавляем переменную под названием state в шаблон, который соответствует значениям варианта Coin::Quarter. Когда Coin::Quarter совпадает, переменная state будет связана с значением штата этой 25-центной монеты. Затем мы можем использовать state в коде для этой ветви, вот так:

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
        }
    }
}

Если мы вызовем value_in_cents(Coin::Quarter(UsState::Alaska)), coin будет равным Coin::Quarter(UsState::Alaska). Когда мы сравниваем это значение с каждой из ветвей match, ни одна из них не совпадает, пока мы не дойдем до Coin::Quarter(state). В этот момент связывание для state будет иметь значение UsState::Alaska. Затем мы можем использовать это связывание в выражении println!, таким образом извлекая внутреннее значение штата из варианта перечисления Coin для Quarter.

Сопоставление с Option{=html}

В предыдущем разделе мы хотели извлечь внутреннее значение T из случая Some, когда использовали Option<T>; мы также можем обрабатывать Option<T> с использованием match, как мы это делали с перечислением Coin! Вместо сравнения монет мы будем сравнивать варианты Option<T>, но принцип работы выражения match остается тем же.

Допустим, мы хотим написать функцию, которая принимает Option<i32> и, если внутри есть значение, добавляет 1 к этому значению. Если внутри нет значения, функция должна вернуть значение None и не пытаться выполнять никакие операции.

Эта функция очень проста в написании благодаря match и будет выглядеть как в Listing 6-5.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
      1 None => None,
      2 Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4

Listing 6-5: Функция, которая использует выражение match для Option<i32>

Давайте более подробно рассмотрим первый вызов plus_one. Когда мы вызываем plus_one(five) [3], переменная x в теле plus_one будет иметь значение Some(5). Затем мы сравниваем это со всеми ветвями match:

None => None,

Значение Some(5) не соответствует шаблону None [1], поэтому мы продолжаем до следующей ветви:

Some(i) => Some(i + 1),

Совпадает ли Some(5) с Some(i) [2]? Да, совпадает! У нас один и тот же вариант. Переменная i связывается с значением, содержащимся в Some, поэтому i получает значение 5. Затем выполняется код в ветви match, поэтому мы добавляем 1 к значению i и создаем новое значение Some с нашим общим значением 6 внутри.

Теперь рассмотрим второй вызов plus_one в Listing 6-5, где x равно None [4]. Мы заходим в match и сравниваем с первой ветвью [1].

Она совпадает! Нет значения, к которому можно было бы добавить что-то, поэтому программа останавливается и возвращает значение None справа от =>. Поскольку первая ветвь совпала, другие ветви не сравниваются.

Комбинация match и перечислений полезна во многих ситуациях. Вы увидите этот паттерн очень часто в коде на Rust: сопоставление с перечислением, связывание переменной с данными внутри и затем выполнение кода на основе них. Сначала это может показаться немного запутанным, но когда вы привыкнете к нему, вы пожелаете, чтобы его было во всех языках. Это постоянно нравится пользователям.

Сопоставления должны быть исчерпывающими

Есть еще один аспект match, о котором нам нужно поговорить: шаблоны ветвей должны охватывать все возможные случаи. Рассмотрим эту версию нашей функции plus_one, которая содержит ошибку и не скомпилируется:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

Мы не обработали случай None, поэтому этот код вызовет ошибку. К счастью, это ошибка, которую Rust умеет обнаруживать. Если мы попытаемся скомпилировать этот код, мы получим эту ошибку:

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
      = 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!(),
    |

Rust знает, что мы не охватили каждый возможный случай и даже знает, какой шаблон мы забыли! Сопоставления в Rust являются исчерпывающими: мы должны исчерпать каждую последнюю возможность, чтобы код был валидным. Особенно в случае Option<T>, когда Rust не позволяет нам забыть явно обработать случай None, он защищает нас от того, чтобы предполагать, что у нас есть значение, когда может быть null, тем самым сделав невозможной ошибку, обсужденную ранее.

Паттерны для захвата всех случаев и символ подстановки _

Используя перечисления, мы также можем выполнять специальные действия для нескольких конкретных значений, а для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы реализуем игру, в которой, если вы выпадает 3 при броске кубика, ваш игрок не двигается, а получает новую роскошную шляпу. Если вы выпадает 7, ваш игрок теряет роскошную шляпу. Для всех остальных значений ваш игрок двигается на такое количество клеток по игровому полю. Вот match, который реализует эту логику, с результатом броска кубика жестко закодированным вместо случайного значения, и всей другой логикой, представленной функциями без тела, потому что фактическое их реализация находится вне области применения данного примера:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
  1 other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

Для первых двух ветвей шаблоны - это литеральные значения 3 и 7. Для последней ветви, которая охватывает все остальные возможные значения, шаблон - это переменная, которую мы выбрали назвать other [1]. Код, который выполняется для ветви other, использует переменную, передав ее в функцию move_player.

Этот код компилируется, хотя мы не перечислили все возможные значения, которые может иметь u8, потому что последний шаблон будет соответствовать всем значениям, не перечисленным специально. Этот шаблон для захвата всех случаев соответствует требованию, что match должен быть исчерпывающим. Обратите внимание, что мы должны поместить ветвь для захвата всех случаев в конец, потому что шаблоны оцениваются по порядку. Если мы поместим ветвь для захвата всех случаев раньше, другие ветви никогда не будут выполняться, поэтому Rust предупредит нас, если мы добавим ветви после ветви для захвата всех случаев!

В Rust также есть шаблон, который мы можем использовать, когда хотим захватить все случаи, но не хотим использовать значение в шаблоне для захвата всех случаев: _ - это специальный шаблон, который соответствует любому значению и не связывается с этим значением. Это сообщает Rust, что мы не собираемся использовать значение, поэтому Rust не будет предупреждать нас о неиспользуемой переменной.

Давайте изменим правила игры: теперь, если вы выпадает что-то, кроме 3 или 7, вам нужно снова бросить кубик. Мы больше не нуждаемся в значении для захвата всех случаев, поэтому мы можем изменить наш код и использовать _ вместо переменной с именем other:

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. Мы можем выразить это, используя единичное значение (пустой тип кортежа, который мы упоминали в разделе "Тип кортежа") в качестве кода, который идет с ветвью _:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

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

Есть еще много о шаблонах и сопоставлении, которое мы рассмотрим в главе 18. На данный момент мы перейдем к синтаксису if let, который может быть полезен в ситуациях, когда выражение match выглядит несколько избыточным.

Резюме

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