使用 if letlet else 实现简洁的控制流

if let 语法让你可以将 iflet 结合起来,以一种更简洁的方式处理匹配某个模式的值,同时忽略其他值。考虑列表 6-6 中的程序,该程序在 config_max 变量上匹配一个 Option<u8> 值,但只有当值是 Some 变体时才执行代码。

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}
Listing 6-6: A match that only cares about executing code when the value is Some

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

相反,我们可以使用 if let 以更简洁的方式编写。以下代码的行为与列表 6-6 中的 match 相同:

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 块中的代码只有在值匹配模式时才会运行。

使用if let意味着更少的输入,更少的缩进,以及更少的样板代码。然而,你会失去match强制的详尽检查。选择matchif let取决于你在特定情况下的操作,以及获得简洁性是否是失去详尽检查的合适权衡。

换句话说,你可以将if let视为一种match的语法糖,它在值匹配一个模式时运行代码,然后忽略所有其他值。

我们可以包含一个 elseif let 一起使用。与 else 一起的代码块与等效于 if letelsematch 表达式中的 _ 情况下的代码块相同。回想一下在清单 6-4 中的 Coin 枚举定义,其中 Quarter 变体还包含一个 UsState 值。如果我们想计算所有非季度硬币,同时宣布季度硬币的州,我们可以使用一个 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 letelse 表达式,如下所示:

#[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 值的硬币的例子,如果我们想根据硬币上州的年龄说一些有趣的话,我们可能会在 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 所示。

Filename: src/main.rs
#[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}");
    }
}
Listing 6-7: Using

这完成了任务,但将工作推到了 if let 语句的主体中,如果要完成的工作更复杂,可能很难跟踪顶级分支之间的关系。我们也可以利用表达式产生值这一事实,从 if let 生成 state 或提前返回,如列表 6-8 所示。(当然,你也可以用 match 做类似的事情!)

Filename: src/main.rs
#[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}");
    }
}
Listing 6-8: Using if let to produce a value or return early.

这以它自己的方式有点令人烦恼!if let的一个分支产生一个值,而另一个分支则完全从函数返回。

为了使这种常见的模式表达得更优雅,Rust 引入了 let-elselet-else 语法在左侧接受一个模式,在右侧接受一个表达式,非常类似于 if let,但它没有 if 分支,只有 else 分支。如果模式匹配,它将在外部作用域中绑定模式中的值。如果模式 匹配,程序将流入 else 分支,该分支必须从函数返回。

在清单 6-9 中,你可以看到使用 let-else 代替 if let 时,清单 6-8 的样子。请注意,这样可以在函数的主要部分保持“在顺利的路径上”,而不会像 if let 那样在两个分支中产生显著不同的控制流。

Filename: src/main.rs
#[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}");
    }
}
Listing 6-9: Using let-else to clarify the flow through the function.

如果你的情况是程序中的逻辑过于冗长,无法用 match 表达,记得 if letlet else 也是你 Rust 工具箱中的选项。

摘要

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

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

为了向用户提供一个组织良好且易于使用的API,并且只暴露用户需要的内容,现在让我们转向Rust的模块。