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

Синтаксис 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}"),
        _ => (),
    }
}
Listing 6-6: match, который выполняет код только когда значение равно Some

Если значение равно 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. Если бы мы хотели посчитать все монеты, не являющиеся четвертью, которые мы видим, одновременно объявляя штат четвертей, мы могли бы сделать это с выражением 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;
    }
}

Оставаться на «счастливом пути» с let...else

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

#[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}");
    }
}
Listing 6-7: Проверка существования штата в 1900 году с использованием вложенных условных операторов внутри if let.

Это решает задачу, но переносит работу в тело оператора 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}");
    }
}
Listing 6-8: Использование if let для получения значения или досрочного возврата.

Но и это не очень удобно для понимания! Одна ветвь 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}");
    }
}
Listing 6-9: Использование let...else для прояснения потока выполнения через функцию.

Обратите внимание, что таким образом мы остаёмся на «счастливом пути» в основном теле функции, без значительно различающегося контроля потока для двух ветвей, как это делал if let.

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

Резюме

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

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

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