使用 if let
和 let else
实现简洁的控制流
if let
语法让你可以将 if
和 let
结合起来,以一种更简洁的方式处理匹配某个模式的值,同时忽略其他值。考虑列表 6-6 中的程序,该程序在 config_max
变量上匹配一个 Option<u8>
值,但只有当值是 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
强制的详尽检查。选择match
和if let
取决于你在特定情况下的操作,以及获得简洁性是否是失去详尽检查的合适权衡。
换句话说,你可以将if let
视为一种match
的语法糖,它在值匹配一个模式时运行代码,然后忽略所有其他值。
我们可以包含一个 else
与 if let
一起使用。与 else
一起的代码块与等效于 if let
和 else
的 match
表达式中的 _
情况下的代码块相同。回想一下在清单 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 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
值的硬币的例子,如果我们想根据硬币上州的年龄说一些有趣的话,我们可能会在 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 所示。
这完成了任务,但将工作推到了 if let
语句的主体中,如果要完成的工作更复杂,可能很难跟踪顶级分支之间的关系。我们也可以利用表达式产生值这一事实,从 if let
生成 state
或提前返回,如列表 6-8 所示。(当然,你也可以用 match
做类似的事情!)
这以它自己的方式有点令人烦恼!if let
的一个分支产生一个值,而另一个分支则完全从函数返回。
为了使这种常见的模式表达得更优雅,Rust 引入了 let
-else
。let
-else
语法在左侧接受一个模式,在右侧接受一个表达式,非常类似于 if let
,但它没有 if
分支,只有 else
分支。如果模式匹配,它将在外部作用域中绑定模式中的值。如果模式 不 匹配,程序将流入 else
分支,该分支必须从函数返回。
在清单 6-9 中,你可以看到使用 let
-else
代替 if let
时,清单 6-8 的样子。请注意,这样可以在函数的主要部分保持“在顺利的路径上”,而不会像 if let
那样在两个分支中产生显著不同的控制流。
如果你的情况是程序中的逻辑过于冗长,无法用 match
表达,记得 if let
和 let else
也是你 Rust 工具箱中的选项。
摘要
我们现在介绍了如何使用枚举来创建可以是枚举值集之一的自定义类型。我们展示了标准库中的Option<T>
类型如何帮助你利用类型系统来防止错误。当枚举值包含数据时,你可以根据需要处理的案例数量,使用match
或if let
来提取和使用这些值。
您的 Rust 程序现在可以使用结构体和枚举来表达领域中的概念。创建自定义类型以在您的 API 中使用可以确保类型安全:编译器将确保您的函数只接收每个函数期望的类型的值。
为了向用户提供一个组织良好且易于使用的API,并且只暴露用户需要的内容,现在让我们转向Rust的模块。