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

Стислий керований потік виконання з if let і let...else (Concise Control Flow with if let and let...else)

Синтаксис if let дає змогу поєднати if і let у менш багатослівний спосіб для обробки значень, що відповідають одному шаблону, ігноруючи решту. Розгляньте програму у Лістингу 6-6, яка зіставляє значення Option<u8> у змінній config_max, але хоче виконати код лише якщо значення — це варіант Some.

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}

Якщо значення — Some, ми виводимо значення у варіанті Some, зв’язуючи значення зі змінною max у шаблоні. Ми не хочемо нічого робити зі значенням None. Щоб задовольнити вираз match, нам доводиться додати _ => () після обробки лише одного варіанта, що є набридливим шаблонним кодом, який потрібно додавати.

Натомість ми могли б записати це коротшим способом, використовуючи if let. Наступний код поводиться так само, як match у Лістингу 6-6:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

Синтаксис if let приймає шаблон і вираз, розділені знаком рівності. Він працює так само, як match, де вираз передається до match, а шаблон є його першою гілкою. У цьому випадку шаблон — Some(max), а max зв’язується зі значенням усередині Some. Потім ми можемо використовувати max у тілі блоку if let так само, як ми використовували max у відповідній гілці match. Код у блоці if let виконується лише якщо значення відповідає шаблону.

Використання if let означає менше набору тексту, менше вкладеності та менше шаблонного коду. Однак ви втрачаєте повну перевірку, яку забезпечує match і яка гарантує, що ви не забули обробити жоден випадок. Вибір між match і if let залежить від того, що ви робите у вашій конкретній ситуації, і від того, чи є отримання лаконічності прийнятним компромісом за втрату повної перевірки.

Іншими словами, ви можете думати про if let як про синтаксичний цукор для match, який запускає код, коли значення відповідає одному шаблону, а потім ігнорує всі інші значення.

Ми можемо включити else з if let. Блок коду, що йде з else, такий самий, як блок коду, що йшов би з випадком _ у виразі match, який еквівалентний if let і else. Згадайте визначення перелічення Coin у Лістингу 6-4, де варіант Quarter також містив значення UsState. Якби ми хотіли рахувати всі монети, що не є quarter, які ми бачимо, одночасно оголошуючи state quarter, ми могли б зробити це за допомогою виразу match, ось так:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

Або ми могли б використати вираз if let і else, ось так:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

Залишатися на “щасливому шляху” (happy path) з let...else

Поширений шаблон — виконати деяке обчислення, коли значення присутнє, і повернути значення за замовчуванням інакше. Продовжуючи наш приклад із монетами зі значенням UsState, якщо ми хотіли б сказати щось смішне залежно від того, наскільки стара state на quarter була, ми могли б додати метод до UsState, щоб перевірити вік state, ось так:

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

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

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Потім ми могли б використати if let, щоб зіставити тип монети, створюючи змінну state у тілі умови, як у Лістингу 6-7.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

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

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Це справляється із завданням, але воно перенесло роботу в тіло оператора if let, і якщо робота, яку потрібно виконати, є складнішою, може бути важко точно простежити, як пов’язані верхньорівневі гілки. Ми також могли б скористатися тим фактом, що вирази створюють значення, або щоб отримати state з if let, або щоб повернутися рано, як у Лістингу 6-8. (Ви могли б зробити щось подібне і з match.)

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

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

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Однак це дещо незручно відстежувати й по-своєму! Одна гілка if let створює значення, а інша повністю повертається з функції.

Щоб зробити цей поширений шаблон приємнішим для вираження, Rust має let...else. Синтаксис let...else приймає шаблон ліворуч і вираз праворуч, дуже подібно до if let, але він не має гілки if, лише гілку else. Якщо шаблон відповідає, він зв’яже значення зі шаблону у зовнішній області видимості. Якщо шаблон не відповідає, програма перейде до гілки else, яка має повернутися з функції.

У Лістингу 6-9 ви можете побачити, як виглядає Лістинг 6-8 при використанні let...else замість if let.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

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

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Зверніть увагу, що таким чином у головному тілі функції він залишається на “щасливому шляху” (happy path), без суттєво відмінного керування потоком виконання для двох гілок, як це було з if let.

Якщо у вас є ситуація, в якій логіка вашої програми є занадто багатослівною, щоб виразити її за допомогою match, пам’ятайте, що if let і let...else також є у вашому інструментарії Rust.

Підсумок

Тепер ми розглянули, як використовувати перелічення для створення власних типів, які можуть бути одним із набору перелічених значень. Ми показали, як стандартна бібліотека Option<T> допомагає вам використовувати систему типів, щоб запобігати помилкам. Коли значення перелічення мають усередині дані, ви можете використовувати match або if let, щоб витягувати й використовувати ці значення, залежно від того, скільки випадків вам потрібно обробити.

Ваші програми Rust тепер можуть виражати поняття у вашій предметній області за допомогою структур і перелічень. Створення власних типів для використання у вашому API забезпечує типову безпеку: компілятор подбає, щоб ваші функції отримували лише значення того типу, який очікує кожна функція.

Щоб надати вашим користувачам добре організований API, який є простим у використанні та відкриває рівно те, що потрібно вашим користувачам, тепер перейдемо до модулів Rust.