Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Конструкция 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() {}
Листинг 6-3: Перечисление и выражение match, образцами в котором служат варианты перечисления

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

Дальше идут ветки match. У ветки две части: образец и код. В первой ветке образец — значение Coin::Penny, а за ним оператор =>, который отделяет образец от кода. Код в данном случае — просто значение 1. Ветки отделяются друг от друга запятыми.

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

Код каждой ветки — это выражение, и значение выражения в совпавшей ветке становится значением всего выражения 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-центовые монеты (quarters), на одной стороне которых было изображение, своё для каждого из 50 штатов. Другим монетам такие изображения не достались, так что это дополнительное значение есть только у четвертаков. Добавим эту информацию в наше enum: пусть вариант Quarter хранит внутри значение UsState, как в листинге 6-4.

#[derive(Debug)] // чтобы чуть позже посмотреть на штат
enum UsState {
    Alabama,
    Alaska,
    // --пропуск--
}

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

fn main() {}
Листинг 6-4: Перечисление Coin, в котором вариант Quarter хранит ещё и значение UsState

Представим, что друг собирает коллекцию четвертаков всех 50 штатов. Сортируя мелочь по типам монет, мы будем заодно называть штат на каждом четвертаке — и если такого у друга ещё нет, он сможет пополнить коллекцию.

В образец ветки для варианта Coin::Quarter мы добавляем переменную state. Когда совпадает 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 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);
}
Листинг 6-5: Функция, применяющая выражение match к Option<i32>

Разберём подробнее первый вызов plus_one. Когда мы вызываем plus_one(five), переменная x в теле plus_one имеет значение Some(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);
}

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, то есть получает значение 5. Затем выполняется код ветки: прибавляем к 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);
}

Совпало! Прибавлять не к чему, поэтому match сразу возвращает None, стоящее справа от =>. Раз первая ветка совпала, остальные уже не проверяются.

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

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
 --> /rustc/88d9e12ae178fab0fb5cc050a94da85685d449ea/library/core/src/option.rs:598:0
 ::: /rustc/88d9e12ae178fab0fb5cc050a94da85685d449ea/library/core/src/option.rs:602:4
  |
  = 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 знает, что мы покрыли не все возможные случаи, и даже знает, какой именно образец мы забыли! Выражения match в Rust исчерпывающие (exhaustive): чтобы код был корректным, нужно перебрать все без исключения варианты. Особенно это важно для Option<T>: не давая забыть про явную обработку None, Rust защищает нас от предположения, что значение есть, когда его может не быть. Так та самая ошибка на миллиард долларов становится невозможной.

Образцы «для всего остального» и заполнитель _

С перечислениями можно делать что-то особенное для нескольких конкретных значений, а для всех остальных — одно действие по умолчанию. Представьте, что мы пишем игру: если на кубике выпало 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. Код этой ветки использует переменную, передавая её в функцию move_player.

Этот код компилируется, хотя мы перечислили не все возможные значения u8: последний образец совпадает со всеми значениями, не перечисленными явно. Такой образец «для всего остального» (catch-all) удовлетворяет требованию исчерпывающей проверки. Заметьте, что ветку «для всего остального» нужно ставить последней: образцы проверяются по порядку. Если поставить её раньше, остальные ветки никогда не выполнятся, поэтому 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. А сейчас перейдём к синтаксису if let, который выручает там, где выражение match получается многословным.