使用 if let 和 let...else 简化控制流
if let 语法将 if 和 let 结合起来,以更简洁的方式处理与某个模式匹配、同时忽略其他情况的值。看示例 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}"),
_ => (),
}
}
Some 时执行代码的 match如果值为 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 中的值。随后,我们可以像在对应的 match 分支中那样,在 if let 代码块中使用 max。只有值与模式匹配时,if let 代码块中的代码才会运行。
使用 if let 可以减少输入、缩进和样板代码。不过,这会失去 match 强制执行的穷尽性检查,而该检查能确保我们没有遗漏任何情况。应根据具体情境以及是否值得用简洁性换取穷尽性检查,在 match 和 if let 之间作出选择。
换句话说,可以把 if let 看作 match 的语法糖:值匹配某个模式时运行代码,其他所有值则被忽略。
我们还可以在 if let 中加入 else。与 else 关联的代码块,相当于等价 match 表达式中的 _ 分支。回想示例 6-4 中的 Coin 枚举定义,其中 Quarter 变体还包含一个 UsState 值。如果我们想统计遇到的所有非 25 美分硬币,同时报出每枚 25 美分硬币代表的州,可以使用 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 值的硬币示例,如果我们想根据 25 美分硬币所代表州的历史长短说些有趣的话,可以在 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 所示。
#[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 中嵌套条件语句,检查某个州在 1900 年是否已经成立这确实完成了任务,但也把工作放进了 if let 语句体中;如果要执行的工作更复杂,就可能难以看清顶层分支之间的关系。我们也可以利用表达式会产生值这一特性,从 if let 中取得 state,或者提前返回,如示例 6-8 所示。(使用 match 也能实现类似写法。)
#[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 生成一个值或提前返回不过,这种写法本身也不太容易理解!if let 的一个分支会产生值,另一个分支则直接从函数返回。
为了更清晰地表达这种常见模式,Rust 提供了 let...else。let...else 的左侧是模式,右侧是表达式,与 if let 非常相似;但它没有 if 分支,只有 else 分支。如果模式匹配,模式中的值会绑定到外层作用域;如果模式不匹配,程序就会进入 else 分支,而该分支必须从函数返回。
示例 6-9 展示了用 let...else 替代 if let 后,示例 6-8 的写法。
#[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}");
}
}
let...else 明确函数中的控制流请注意,这种写法让函数主体保持在“顺畅路径”上,不像 if let 那样让两个分支的控制流差异很大。
如果程序中的逻辑用 match 表达起来过于冗长,请记住 Rust 工具箱中还有 if let 和 let...else。
总结
现在,我们已经介绍了如何使用枚举创建只能取一组枚举值之一的自定义类型,也说明了标准库的 Option<T> 类型如何帮助你利用类型系统防止错误。枚举值包含数据时,可以根据需要处理的情况数量,使用 match 或 if let 提取并使用这些值。
现在,你可以使用结构体和枚举在 Rust 程序中表达领域概念。为 API 创建自定义类型可以确保类型安全:编译器会保证函数只接收到该函数所期望类型的值。
为了向用户提供组织良好、易于使用且只公开用户所需内容的 API,接下来我们将学习 Rust 的模块。