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 letlet...else 的简洁控制流

Concise Control Flow with if let and let...else

if let 语法让你可以将 iflet 结合成一种更不冗长的方式,来处理匹配一个模式的值,同时忽略其余模式。考虑示例 6-6 中的程序,它在 config_max 变量中匹配 Option<u8> 值,但只想在值是 Some 变体时执行代码。

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest. Consider the program in Listing 6-6 that matches on an Option<u8> value in the config_max variable but only wants to execute code if the value is the Some variant.

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}

如果值是 Some,我们通过在模式中将值绑定到变量 max 来打印出 Some 变体中的值。我们不想对 None 值做任何事情。为了满足 match 表达式,我们必须在仅处理一个变体后添加 _ => (),这是添加起来很烦人的样板代码。

If the value is Some, we print out the value in the Some variant by binding the value to the variable max in the pattern. We don’t want to do anything with the None value. To satisfy the match expression, we have to add _ => () after processing just one variant, which is annoying boilerplate code to add.

相反,我们可以使用 if let 以更短的方式编写这段代码。以下代码的行为与示例 6-6 中的 match 相同:

Instead, we could write this in a shorter way using if let. The following code behaves the same as the match in Listing 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 内部的值。然后我们可以在 if let 块的正文中使用 max,方式与在相应的 match 分支中使用 max 相同。if let 块中的代码仅在值匹配模式时运行。

The syntax if let takes a pattern and an expression separated by an equal sign. It works the same way as a match, where the expression is given to the match and the pattern is its first arm. In this case, the pattern is Some(max), and the max binds to the value inside the Some. We can then use max in the body of the if let block in the same way we used max in the corresponding match arm. The code in the if let block only runs if the value matches the pattern.

使用 if let 意味着更少的输入、更少的缩进和更少的样板代码。然而,你失去了 match 强制执行的穷尽性检查,该检查确保你没有忘记处理任何情况。在 matchif 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 的语法糖,它在值匹配一个模式时运行代码,然后忽略所有其他值。

In other words, you can think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.

我们可以在 if let 中包含 else。与 else 搭配的代码块与在等效于 if letelsematch 表达式中与 _ 情况搭配的代码块相同。回想示例 6-4 中的 Coin 枚举定义,其中 Quarter 变体还持有一个 UsState 值。如果我们想在宣布 25 美分硬币所属州的同时,也清点我们看到的所有非 25 美分硬币,我们可以用 match 表达式这样做:

We can include an else with an if let. The block of code that goes with the else is the same as the block of code that would go with the _ case in the match expression that is equivalent to the if let and else. Recall the Coin enum definition in Listing 6-4, where the Quarter variant also held a UsState value. If we wanted to count all non-quarter coins we see while also announcing the state of the quarters, we could do that with a match expression, like this:

#[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 letelse 表达式,像这样:

Or we could use an if let and else expression, like this:

#[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 保持在“快乐路径”上

Staying on the “Happy Path” with let...else

常见的模式是当值存在时执行某些计算,否则返回一个默认值。继续我们带有 UsState 值的硬币示例,如果我们想根据 25 美分硬币上的州有多古老来说些有趣的话,我们可能会在 UsState 上引入一个方法来检查州的年龄,如下所示:

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)] // 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 所示。

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)] // 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 ,要么提前返回,如示例 6-8 所示。(你也可以用 match 做类似的事情。)

That 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)] // 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 的一个分支产生一个值,而另一个分支则完全从函数返回。

This is a bit annoying to follow in its own way, though! One branch of the if let produces a value, and the other one returns from the function entirely.

为了使这种常见的模式更漂亮地表达,Rust 提供了 let...elselet...else 语法在左侧接收一个模式,在右侧接收一个表达式,与 if let 非常相似,但它没有 if 分支,只有 else 分支。如果模式匹配,它将在外部作用域中绑定来自模式的值。如果模式“不”匹配,程序将流入 else 分支,该分支必须从函数返回。

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.

在示例 6-9 中,你可以看到使用 let...else 代替 if let 时示例 6-8 看起来是什么样子的。

In Listing 6-9, you can see how Listing 6-8 looks when using let...else in place of 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 那样为两个分支提供显着不同的控制流。

Notice 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.

如果你遇到程序逻辑过于冗长而无法使用 match 表达的情况,请记住 if letlet...else 也在你的 Rust 工具箱中。

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.

总结

Summary

我们现在已经介绍了如何使用枚举来创建自定义类型,这些类型可以是枚举值集合中的一个。我们展示了标准库的 Option<T> 类型如何帮助你利用类型系统来预防错误。当枚举值内部包含数据时,你可以根据需要处理的情况数量,使用 matchif let 来提取并使用这些值。

We’ve now covered how to use enums to create custom types that can be one of a set of enumerated values. We’ve shown how the standard library’s Option<T> type helps you use the type system to prevent errors. When enum values have data inside them, you can use match or if let to extract and use those values, depending on how many cases you need to handle.

你的 Rust 程序现在可以使用结构体和枚举来表达你领域中的概念。创建要在 API 中使用的自定义类型可确保类型安全:编译器将确保你的函数仅获得每个函数所期望类型的值。

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.

为了向你的用户提供组织良好、易于使用且仅公开用户确切需要内容的 API,现在让我们转向 Rust 的模块(modules)。

In order to provide a well-organized API to your users that is straightforward to use and only exposes exactly what your users will need, let’s now turn to Rust’s modules.