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. Вспомните определение enum Coin из листинга 6-4, где вариант Quarter также хранил значение UsState. Если бы мы хотели подсчитывать все монеты, не являющиеся 25-центовыми, и одновременно объявлять штат для 25-центовых монет, мы могли бы сделать это с помощью выражения 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, если бы мы хотели сказать что-то забавное в зависимости от того, насколько старым был штат на 25-центовой монете, мы могли бы добавить метод для 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.

Итоги

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

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

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