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}"),
        _ => (),
    }
}
Листинг 6-6: match, которому важно выполнить код только для значения 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, которому передали это выражение и у которого этот образец — первая ветка. Здесь образец — Some(max), и max привязывается к значению внутри Some. Дальше max можно использовать в теле блока if let точно так же, как в соответствующей ветке match. Код в блоке if let выполняется, только если значение совпало с образцом.

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

Иначе говоря, if let можно считать синтаксическим сахаром для match, который выполняет код, когда значение совпадает с одним образцом, и игнорирует все остальные значения.

К if let можно добавить else. Блок кода после else — то же самое, что код ветки _ в равнозначном выражении match. Вспомните определение перечисления Coin из листинга 6-4, где вариант Quarter хранил ещё и значение UsState. Если бы мы хотели посчитать все монеты, кроме четвертаков, и при этом называть штат на каждом четвертаке, это можно было бы сделать выражением match:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --пропуск--
}

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,
    // --пропуск--
}

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)] // чтобы чуть позже посмотреть на штат
enum UsState {
    Alabama,
    Alaska,
    // --пропуск--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- пропуск --
        }
    }
}

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)] // чтобы чуть позже посмотреть на штат
enum UsState {
    Alabama,
    Alaska,
    // --пропуск--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- пропуск --
        }
    }
}

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}");
    }
}
Листинг 6-7: Проверяем, существовал ли штат в 1900 году, с помощью условий внутри if let

Задачу это решает, но вся работа оказалась внутри тела if let. Если бы работа была сложнее, было бы трудно уследить, как связаны между собой ветки верхнего уровня. Можно воспользоваться тем, что выражения возвращают значение: либо получить state из if let, либо досрочно выйти из функции, как в листинге 6-8. (С match можно сделать нечто похожее.)

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

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- пропуск --
        }
    }
}

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}");
    }
}
Листинг 6-8: if let либо выдаёт значение, либо досрочно выходит из функции

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

Чтобы такой частый случай записывался приятнее, в Rust есть let...else. Синтаксис let...else очень похож на if let: слева образец, справа выражение. Но ветки if у него нет — только ветка else. Если образец совпал, значение из образца привязывается к переменной во внешней области видимости. Если не совпал, выполнение переходит в ветку else, которая обязана выйти из функции.

В листинге 6-9 показано, как выглядит листинг 6-8, если вместо if let использовать let...else.

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

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- пропуск --
        }
    }
}

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}");
    }
}
Листинг 6-9: let...else делает ход выполнения функции нагляднее

Обратите внимание: основной код функции остаётся на «счастливом пути» (happy path). В нём больше нет развилки, где одна ветка выдаёт значение, а другая выходит из функции, как было с if let.

Если логику программы слишком многословно выражать через match, помните, что в вашем арсенале есть ещё if let и let...else.

Итоги

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

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

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