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 (The match Control Flow Construct)

Rust має надзвичайно потужну конструкцію керування потоком, що називається match, яка дозволяє вам порівнювати значення з послідовністю шаблонів (patterns) і потім виконувати код на основі того, який шаблон збігається. Шаблони можуть складатися з літеральних значень, імен змінних, символів підстановки для будь-чого, і багато чого іншого; Розділ 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.

Зазвичай ми не використовуємо фігурні дужки, якщо код у гілці 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 рік Сполучені Штати карбували монети quarter з різним дизайном для кожного з 50 штатів на одному боці. Жодні інші монети не мали дизайну штатів, тож лише quarter мають це додаткове значення. Ми можемо додати цю інформацію до нашого перелічення (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() {}

Уявімо, що друг намагається зібрати всі 50 state quarters. Поки ми сортуємо дріб’язок за типом монети, ми також називатимемо назву штату, пов’язаного з кожною quarter, щоб якщо це одна з тих, якої у нашого друга немає, він міг додати її до своєї колекції.

У виразі match для цього коду ми додаємо змінну під назвою state до шаблону, який відповідає значенням варіанта Coin::Quarter. Коли Coin::Quarter збігається, змінна state прив’яжеться до значення штату цієї quarter. Потім ми можемо використати 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!, таким чином отримавши внутрішнє значення штату з варіанта Coin для Quarter.

Зіставлення match з 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);
}

Розгляньмо перше виконання 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. Потім виконується код у гілці 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 проти перелічення, прив’язування змінної до даних всередині, а потім виконання коду на основі цього. Спочатку це трохи складно, але після того, як ви звикнете до цього, ви захочете мати це в усіх мовах. Це послідовно є улюбленим серед користувачів.

Зіставлення вичерпне

Є ще один аспект 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/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597: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, тому що останній шаблон збігатиметься з усіма значеннями, не переліченими явно. Цей універсальний шаблон (catch-all pattern) виконує вимогу, що 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. Ми можемо виразити це, використавши одиничне значення (unit value) (порожній тип кортежу, який ми згадували в розділі 3) як код, що йде з гілкою _:

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 трохи багатослівний.