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!("Максимум выставлен на {max}"),
_ => (),
}
}
match that only cares about executing code when the value is 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!("Максимум выставлен на {max}");
}
}
Синтаксис if let принимает шаблон и выражение, разделённые знаком равенства. Он работает так же, как match, которому на вход подают выражение, отвечающее шаблону первой ветви. В данном случае шаблоном является Some(max), где max привязывается к значению внутри Some. После сопоставления, мы можем использовать max в теле блока if let так же, как мы использовали max в соответствующей ветке match. Код в блоке if let не запускается, если значение не отвечает шаблону.
Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking match enforces that ensures that you aren’t forgetting to handle any cases. Choosing between match and if let depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.
Другими словами, вы можете думать о конструкции if let как о синтаксическом сахаре для match, выполняющем код, если входное значение будет отвечать единственному шаблону, и проигнорирует все остальные значения.
Можно добавлять else к if let. Блок кода, который находится внутри else, аналогичен по смыслу блоку кода ветви выражения match (если оно эквивалентно сборной конструкции if let и else), связанной с шаблоном _. Вспомним объявление перечисления 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:?}!"),
_ => 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:?}!");
} else {
count += 1;
}
}
Staying on the “Happy Path” with let...else
The common pattern is to perform some computation when a value is present and return a default value otherwise. Continuing with our example of coins with a UsState value, if we wanted to say something funny depending on how old the state on the quarter was, we might introduce a method on UsState to check the age of a state, like so:
#[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:?} довольно староват для Америки!"))
} else {
Some(format!("Штат {state:?} относительно молодой."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
Then, we might use if let to match on the type of coin, introducing a state variable within the body of the condition, as in Listing 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:?} довольно староват для Америки!"))
} else {
Some(format!("Штат {state:?} относительно молодой."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
if letThat gets the job done, but it has pushed the work into the body of the if let statement, and if the work to be done is more complicated, it might be hard to follow exactly how the top-level branches relate. We could also take advantage of the fact that expressions produce a value either to produce the state from the if let or to return early, as in Listing 6-8. (You could do something similar with a match, too.)
#[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:?} довольно староват для Америки!"))
} else {
Some(format!("Штат {state:?} относительно молодой."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
if let to produce a value or return earlyОднако и это решение несколько раздражает, поскольку каждая ветвь if let ведёт себя совершенно по-своему! Одна ветвь лишь вычисляется в значение, а другая прерывает всю функцию и сразу из неё что-то возвращает.
To make this common pattern nicer to express, Rust has let...else. The let...else syntax takes a pattern on the left side and an expression on the right, very similar to if let, but it does not have an if branch, only an else branch. If the pattern matches, it will bind the value from the pattern in the outer scope. If the pattern does not match, the program will flow into the else arm, which must return from the function.
In Listing 6-9, you can see how Listing 6-8 looks when using let...else in place of if let.
#[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:?} довольно староват для Америки!"))
} else {
Some(format!("Штат {state:?} относительно молодой."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
let...else to clarify the flow through the functionNotice that it stays on the “happy path” in the main body of the function this way, without having significantly different control flow for two branches the way the if let did.
If you have a situation in which your program has logic that is too verbose to express using a match, remember that if let and let...else are in your Rust toolbox as well.
Подведём итоги
Мы рассмотрели, как использовать перечисления для создания пользовательских типов, представляющий перечень возможных вариантов. Мы показали, как тип стандартной библиотеки Option<T> помогает использовать систему типов для предотвращения ошибок. Если варианты перечисления имеют данные внутри них, можно использовать match или if let (смотря, сколько случаев вам нужно обработать), чтобы извлечь их и воспользоваться ими.
Your Rust programs can now express concepts in your domain using structs and enums. Creating custom types to use in your API ensures type safety: The compiler will make certain your functions only get values of the type each function expects.
Теперь пришла пока поговорить о модулях в Rust. Они нужны, чтобы предоставлять пользователям вашего кода хорошо организованный API, который прост в использовании и предоставляет только то, что нужно.