简洁的控制流:if let 和 let else
if let 语法让你可以将 if 和 let 结合起来,以更简洁的方式处理匹配某一模式的值,同时忽略其他情况。考虑清单6-6中的程序,它对 config_max 变量中的 Option<u8> 值进行匹配,但只在值是 Some 变体时执行代码。
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
清单6-6:仅在值为 Some 时执行代码的 match
如果值是 Some,我们通过模式绑定将其中的值赋给变量 max 并打印出来。对于 None 值我们不做任何操作。为了满足 match 表达式,我们必须在处理完一个变体后添加 _ => ()
,这是一段令人烦恼的样板代码。
相反,我们可以用 if let 写得更短一些。下面这段代码与清单6-6中的 match 行为相同:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
if let 的语法由一个模式和一个表达式组成,中间用等号分隔。它工作原理类似于 match,其中表达式作为被匹配对象,模式则是第一个分支。在这里,模式是 Some(max)
,max 会绑定到 Some 中包含的值,然后我们就可以像使用对应 match 分支里的 max 一样,在 if let 块中使用它。只有当值匹配该模式时,if let 块内的代码才会运行。
使用 if let 意味着输入更少、缩进更浅、样板代码也减少了。但这样做会失去 match 所强制执行的穷尽检查。在具体场景下选择使用 match 或 if let,要看是否愿意以牺牲穷尽性检查换取简洁性。
换句话说,可以把 if let 看作一种语法糖,用来写只针对某个特定模式运行代码,而忽略所有其他情况的 match。
我们还可以给 if let 加上 else 分支。这部分与等价于该 if let…else 的 match 表达式中 _
情况对应。例如回想清单6-4中定义了 Coin 枚举,其中 Quarter 变体还携带了 UsState 值。如果想统计非 quarter 硬币数量,同时报告 quarter 来自哪个州,可以用如下match:
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => count += 1,
}
或者用 if let 和 else 实现:
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}
保持“顺畅路径”——let…else
常见的一种编程习惯是在有有效值时进行计算,否则返回默认结果。继续之前关于含 UsState 的硬币例子,如果想根据季度硬币所属州存在多久来说点幽默的话,可以给 UsState 添加方法判断某年该州是否已存在,如下:
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- 略 --
}
}
}
然后利用 if let 匹配硬币类型,并在条件块内引入 state,如清单6-7所示。
文件名:src/main.rs
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
}
}
清单6-7:通过嵌套条件判断状态是否存在于1900年
这种写法能完成任务,但把逻辑都塞进了 if-let 内部,如果逻辑复杂可能难以理解顶层分支之间关系。同样,也可利用表达式产生返回值这一特点,通过早期返回实现,如清单6-8。(match 同理)
文件名:src/main.rs
fn describe_state_quarter(coin: Coin) -> Option<String> {
// 如果不是 Quarter,则提前返回 None;否则提取出 state。
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."))
}
}
清单6-8:利用if-let生成value或提前return
不过,这种写法本身也稍显繁琐!因为一边产生 value,一边直接从函数返回,两条分支控制流差异较大。
为了让这种常见写法更加优雅,Rust 引入了 let...else
。其语法左侧为模式,右侧为表达式,与 if-let
类似,但没有独立 “if” 分支,仅有 “else”。若匹配成功,会将绑定变量放置外层作用域;若失败,则进入必须从函数退出(如 return)的 else 分支。
看一下如何用 let...else
改写前面例子(即替代 Listing 6-8),请参见清单6-9。这种方式使主流程保持“顺畅路径”,避免因两条不同控制流而导致混乱:
文件名:src/main.rs
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."))
}
}
清单6-9:使用let…else澄清函数流程
如果遇到需要比match 更冗长才能描述逻辑的问题,不妨记住 Rust 工具箱里还有 if-let
与 let...else
可供选择。
总结
至此,我们介绍了如何借助枚举创建可选多态类型,以及标准库提供帮助防止错误发生的 Option 类型。当枚举携带数据时,可根据需求选择使用match或if-let解构并访问这些数据,从而灵活应对各种情况。
你的 Rust 程序现在能够通过结构体和枚举准确地表示领域概念,为 API 创建自定义类型确保类型安全——编译器保证每个函数只能接收预期的数据类型参数。
为了向用户提供组织良好且易于使用,只暴露必要内容的软件接口,让我们接下来转向学习 Rust 模块系统吧。