Краткое управление потоком с 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}"),
_ => (),
}
}
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}");
}
}
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}");
}
}
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}");
}
}
let...else, чтобы сделать поток выполнения функции понятнееОбратите внимание, что так основной текст функции остается на «счастливом
пути»: без двух ветвей с существенно разным потоком управления, как это было с
if let.
Если у вас возникла ситуация, в которой логика программы слишком многословна
для выражения с помощью match, помните, что if let и let...else тоже
есть в вашем наборе инструментов Rust.
Итоги
Теперь мы рассмотрели, как использовать enum для создания пользовательских
типов, которые могут быть одним из набора перечисленных значений. Мы показали,
как тип Option<T> из стандартной библиотеки помогает использовать систему
типов для предотвращения ошибок. Когда значения enum содержат данные внутри
себя, можно использовать match или if let, чтобы извлечь и использовать
эти значения, в зависимости от того, сколько случаев нужно обработать.
Теперь ваши программы на Rust могут выражать понятия из вашей предметной области с помощью структур и enum. Создание пользовательских типов для использования в вашем API обеспечивает типобезопасность: компилятор проследит, чтобы ваши функции получали только значения того типа, который каждая функция ожидает.
Чтобы предоставить пользователям хорошо организованный API, который прост в использовании и открывает только то, что им действительно понадобится, теперь перейдем к модулям Rust.