前一篇文章我们对 Rust 的基本类型已经有了相当多的了解,那么接下来,趁热打铁,我们将重点关注 Rust 的类型系统之模式匹配——从结构中提取数据。
不可反驳的模式(Irrefutable Patterns)
“不可反驳的模式”(irrefutable patterns)指的是那些在模式匹配中总是成功匹配,不会失败的模式。
上一篇文章,我们简要了解了如何使用模式来解构复合值。下面让我们回顾一下,并且讨论模式还能表达的其他一些东西:
fn takes_tuple(tuple: (char, i32, bool)) {
let a = tuple.0;
let b = tuple.1;
let c = tuple.2;
// 这和上面的做法是一样的。
let (a, b, c) = tuple;
// 忽略第一个元素,只绑定第二个和第三个元素。
let (_, b, c) = tuple;
// 除了最后一个元素,其他的都忽略。
let (.., c) = tuple;
}
fn main() {
takes_tuple(('a', 777, true));
}
- 以上的匹配模式均是不可反驳的模式,也就是说,它们总能与等号右边的值匹配上。
- 变量名是一种总能匹配的模式,它将匹配到的值绑定到一个具有该名称的新变量中。
_是一种总能匹配任何值的模式,它会丢弃匹配到的值,这和 Kotlin 中的_意义一样。..允许你一次性忽略多个值。
我们还可以使用 .. 表示更高级的用法,比如忽略元组中间的元素:
fn takes_tuple(tuple: (char, i32, bool, u8)) {
let (first, .., last) = tuple;
}
同样也适用于数组:
fn takes_array(array: [u8; 5]) {
let [first, .., last] = array;
}
匹配值
match 关键字允许你将一个值与一个或多个模式进行匹配。这些模式可以是简单的值,也可以用于表达更复杂的条件。类似于 C 和 C++ 中的 switch,或者 Kotlin 中的 when,只不过 Rust 中匹配更加强大:
fn main() {
let input = 'x';
match input {
'q' => println!("Quitting"),
'a' | 's' | 'w' | 'd' => println!("Moving around"),
'0'..='9' => println!("Number input"),
key if key.is_lowercase() => println!("Lowercase: {key}"),
_ => println!("Something else"),
}
}
// Output
// Lowercase: x
模式中的变量(在此示例中为 key)将创建一个可在 match 分支内使用的绑定(你可以认为一个临时变量,在当前分支中 key 就是 x)。
匹配守卫(match guard,第4个分支的 if key.is_lowercase() 就是匹配守卫)会使分支只有在条件为真时才匹配。如果条件为假,match 将继续检查后续的情况。
你可能已经注意到模式中使用某些特定字符的方式:
|用作 “或” 操作符..可根据需要进行扩展1..=5表示一个闭区间_是通配符
当我们希望简洁地表达比单独使用模式更复杂的想法时,匹配守卫很重要且必要。
匹配守卫不同于 match 分支内部的单独 if 表达式。分支块(=> 之后)中的 if 表达式是在选择了 match 分支之后才执行的。如果该块内的 if 条件不成立,并不会导致重新考虑原始 match 表达式的其他分支。
守卫中定义的条件适用于带有 | 的模式中的每个表达式。
另一种模式语法是 @ 语法,它将模式的一部分绑定到一个变量:
let opt = Some(123);
match opt {
outer @ Some(inner) => {
println!("outer: {outer:?}, inner: {inner}");
}
None => {}
}
// Output
// outer: Some(123), inner: 123
在这个例子中,inner 的值为 123,它是通过解构从 Option 中提取出来的。outer 捕获了整个 Some(inner) 表达式,因此它包含完整的 Option::Some(123) 。这种用法很少见,但在更复杂的模式中可能会很有用。
解构结构体
和元组一样,结构体也可以通过模式匹配来解构:
struct Foo {
x: (u32, u32),
y: u32,
}
#[rustfmt::skip]
fn main() {
let foo = Foo { x: (1, 2), y: 3 };
match foo {
Foo { x: (1, b), y } => println!("x.0 = 1, b = {b}, y = {y}"),
Foo { y: 2, x: i } => println!("y = 2, x = {i:?}"),
Foo { y, .. } => println!("y = {y}, other fields were ignored"),
}
}
// Output
// x.0 = 1, b = 2, y = 3
解构枚举
和元组一样,枚举也可以通过模式匹配来解构。模式也可以用于将变量绑定到值的各个部分。这就是检查类型结构的方法。让我们从一个简单的枚举类型开始:
enum Result {
Ok(i32),
Err(String),
}
fn divide_in_two(n: i32) -> Result {
if n % 2 == 0 {
Result::Ok(n / 2)
} else {
Result::Err(format!("cannot divide {n} into two equal parts"))
}
}
fn main() {
let n = 100;
match divide_in_two(n) {
Result::Ok(half) => println!("{n} divided in two is {half}"),
Result::Err(msg) => println!("sorry, an error happened: {msg}"),
}
}
// Output
// 100 divided in two is 50
在这里,我们使用分支来解构 Result 值。在第一个分支中,half 被绑定到 Ok 变体中的值。在第二个分支中,msg 被绑定到错误消息。
let 控制流
Rust 有一些与其他语言不同的控制流结构。它们用于模式匹配:
if let表达式while let表达式let else表达式
if let 表达式允许你根据一个值是否匹配某个模式来执行不同的代码:
use std::time::Duration;
fn sleep_for(secs: f32) {
if let Ok(duration) = Duration::try_from_secs_f32(secs) {
std::thread::sleep(duration);
println!("slept for {duration:?}");
}
}
fn main() {
sleep_for(-10.0);
sleep_for(0.8);
}
// Output
// slept for 800.000012ms
与 match 不同,if let 不必涵盖所有分支。这使得它比 match 更加简洁。
一个常见的用法是在处理 Option 类型时处理 Some 值。
if let 不支持用于模式匹配的守卫子句,通过添加 else 子句,它可以用作一个表达式。
与 if let 类似,while let 变体可以针对某个模式重复测试一个值:
fn main() {
let mut name = String::from("Comprehensive Rust 🦀");
while let Some(c) = name.pop() {
dbg!(c);
}
}
在这里,String::pop 会一直返回 Some(c),直到字符串为空,之后它将返回 None。while let 让我们能够不断遍历所有元素。
需要指出的是,只要值与模式匹配,while let 循环就会一直继续下去。
你可以将 while let 循环改写为一个无限循环(还记得 Rust 有一个叫 loop 的循环吗?),在循环内部使用 if 语句,当 name.pop() 没有值可解包时跳出循环。while let 为这样的场景提供了语法糖。
这种形式不能用作表达式,因为如果条件为假,它可能没有值。
对于匹配模式并从函数返回的常见情况,可以使用 let else。else 分支必须有终止操作(返回、跳出循环或 panic —— 只要不是正常执行完代码块即可)。
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let Some(s) = maybe_string else {
return Err(String::from("got None"));
};
let Some(first_byte_char) = s.chars().next() else {
return Err(String::from("got empty string"));
};
let Some(digit) = first_byte_char.to_digit(16) else {
return Err(String::from("not a hex digit"));
};
Ok(digit)
}
fn main() {
println!("result: {:?}", hex_or_die_trying(Some(String::from("foo"))));
}
// Output
// result: Ok(15)
如果你看不太明白表达的意思,我是用 if let 该写下你就懂了:
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let s = if let Some(s) = maybe_string {
s
} else {
return Err(String::from("got None"));
};
let first_byte_char = if let Some(first) = s.chars().next() {
first
} else {
return Err(String::from("got empty string"));
};
let digit = if let Some(digit) = first_byte_char.to_digit(16) {
digit
} else {
return Err(String::from("not a hex digit"));
};
Ok(digit)
}
这种基于提前返回的控制流在 Rust 错误处理代码中很常见。在这种代码中,你尝试从 Result 中获取一个值,如果 Result 是 Err,则返回一个错误。