Оператор управления потоком выполнения match

В Rust есть невероятно мощная конструкция управления потоком выполнения программы под названием match, которая позволяет вам сравнивать значение с серией шаблонов и затем выполнять код, связанный с совпавшим шаблоном. Шаблонами могут выступать литералы, имена переменных, подстановочные значения и многое другое; в Главе 18 описаны все разновидности шаблонов и что они делают. Сила 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() {}

Листинг 6-3: Перечисление и выражение match, которое использует варианты перечисления в качестве шаблонов

Давайте разберём match в функции value_in_cents. Сначала пишется ключевое слово match, затем следует выражение, которое в данном случае является значением coin. Это выглядит очень похоже на выражение if, но есть большая разница: с if выражение должно возвращать булево значение, а здесь это может быть любой тип. Тип coin в этом примере - перечисление типа Coin, объявленное в строке 1.

Далее идут ветки match. Ветки состоят из двух частей: шаблон и некоторый код. Здесь первая ветка имеет шаблон, который является значением Coin::Penny, затем идёт оператор =>, который разделяет шаблон и код для выполнения. Код в этом случае - это просто значение 1. Каждая ветка отделяется от последующей при помощи запятой.

Когда выполняется выражение match, оно сравнивает полученное значение с образцом каждой ветки по порядку. Если шаблон совпадает со значением, то выполняется код, связанный с этим шаблоном. Если этот шаблон не соответствует значению, то выполнение продолжается со следующей ветки, так же, как в автомате по сортировке монет. У нас может быть столько веток, сколько нужно: в листинге 6-3 наш match состоит из четырёх веток.

Код, связанный с каждой веткой, является выражением, а полученное значение выражения в соответствующей ветке — это значение, которое возвращается для всего выражения match.

Фигурные скобки обычно не используются, если код ветки короткий, как в листинге 6-3, где каждая ветка только возвращает значение. Если необходимо выполнить несколько строк кода в ветке, можно использовать фигурные скобки. Например, следующий код будет выводить «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 год Соединённые Штаты чеканили 25 центов с различным дизайном на одной стороне для каждого из 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() {}

Листинг 6-4: Перечисление 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). Когда мы будем сравнивать это значение с каждой из веток, ни одна из них не будет совпадать, пока мы не достигнем варианта 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);
}

Листинг 6-5: Функция, которая использует выражение 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. Затем выполняется код ассоциированный для данной ветки, поэтому мы добавляем 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) не подразумевает наличие какого-то значения к которому можно было бы что-то добавить, поэтому программа останавливается и возвращает значение которое находится справа от => - т.е. None. Так как шаблон первой ветки совпал, то никакие другие шаблоны веток не сравниваются.

Комбинирование match и перечислений полезно во многих ситуациях. Вы много где сможете увидеть подобный шаблон в коде программ на Rust: сделать сопоставление значения используя один из шаблонов 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
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums`

To learn more, run the command again with --verbose.

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

Заполнитель _

В Rust также есть шаблон, который можно использовать, когда не хочется перечислять все возможные значения. Например, u8 может иметь допустимые значения от 0 до 255. Если мы только заботимся о значениях 1, 3, 5 и 7 и не хотим перечислять 0, 2, 4, 6, 8, 9 вплоть до 255, то к счастью нам это не нужно. Вместо этого можно использовать специальный шаблон _:

fn main() {
    let some_u8_value = 0u8;
    match some_u8_value {
        1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),
    }
}

Шаблон с заполнителем _ будет соответствовать любому значению. Поместив его после наших других веток, ветка с заполнителем _ будет соответствовать всем возможным случаям, которые не были указаны ранее. Так как () это просто значение единичного типа, то в случае _ ничего не произойдёт. В результате можно сказать, что мы хотим ничего не делать для всех возможных значений, которые мы не обработали в списке перед _ заполнителем.

Тем не менее, выражение match может быть несколько многословным в ситуации, в которой важен только один из случаев. Для этой ситуации Rust предоставляет if let выражение.

Подробнее о шаблонах и сопоставлении с образцом можно найти в Главе 18.