Стислий керований потік виконання з 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.