第19章 模式与模式匹配
模式(pattern)是 Rust 中一种特殊的语法,用来匹配类型的结构,无论类型简单还是复杂。将模式与 match 表达式及其他结构结合使用,可以让你对程序的控制流有更多掌控。模式由以下内容中的某种组合构成:
- 字面值
- 解构后的数组、枚举、结构体或元组
- 变量
- 通配符
- 占位符
模式的一些例子包括 x、(a, 3) 和 Some(Color::Red)。在允许使用模式的上下文中,这些组成部分描述了数据的形状。程序随后会让值与模式匹配,以决定它是否具有继续运行某段特定代码所需的数据形状。
要使用模式,我们会把它与某个值进行比较。如果模式匹配该值,就能在代码中使用这个值的各个部分。回忆一下第六章讨论 match 表达式时用到模式的情形,比如硬币分类器的例子。如果值符合模式的形状,就可以使用其中那些被命名的部分;如果不符合,与该模式关联的代码就不会运行。
本章可以视为一份与模式有关内容的参考。我们会介绍哪些位置可以合法使用模式,可反驳(refutable)模式与不可反驳(irrefutable)模式之间的区别,以及你可能见到的各种模式语法。到本章末尾,你将掌握如何用模式以清晰的方式表达许多概念。
所有可能会用到模式的位置
所有可以使用模式的位置
模式会出现在 Rust 的很多地方,而你可能已经在不知不觉中用了很多次!本节会讨论所有可以合法使用模式的位置。
match 分支
正如第六章讨论过的,我们会在 match 表达式的分支中使用模式。从形式上看,match 表达式由 match 关键字、要匹配的值,以及一个或多个 match 分支组成;这些分支包含一个模式,以及当值匹配该分支模式时要运行的表达式,就像这样:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
例如,下面是示例 6-5 中那个匹配变量 x 内 Option<i32> 值的 match 表达式:
match x {
None => None,
Some(i) => Some(i + 1),
}
这个 match 表达式中的模式,就是每个箭头左边的 None 和 Some(i)。
match 表达式有一个要求,那就是它必须是穷尽的(exhaustive):match 表达式中值的所有可能情况都必须被覆盖到。确保覆盖所有可能性的一种方式,是让最后一个分支使用“捕获所有”的模式;例如,一个可以匹配任意值的变量名永远不会失败,因此它能够覆盖所有剩余情况。
特定的模式 _ 可以匹配任何东西,但它永远不会绑定到变量上,因此常被用于最后一个 match 分支。当你想忽略某个未指定的值时,_ 模式会非常有用。稍后在本章的 “忽略模式中的值” 一节中,我们会更详细地讨论 _ 模式。
let 语句
在本章之前,我们只明确讨论过在 match 和 if let 中使用模式,但实际上,我们也在其他地方使用过模式,包括 let 语句。例如,来看这个简单直接的变量赋值:
#![allow(unused)]
fn main() {
let x = 5;
}
每次你写出像这样的 let 语句时,其实都在使用模式,只是你可能没有意识到!更正式地说,let 语句看起来像这样:
let PATTERN = EXPRESSION;
在像 let x = 5; 这样的语句中,位于 PATTERN 位置的变量名只是模式的一种特别简单的形式。Rust 会拿表达式与模式进行比较,并将它找到的任何名字赋值。所以,在 let x = 5; 这个例子中,x 是一个模式,表示“把这里匹配到的内容绑定到变量 x”。因为名字 x 本身就是整个模式,所以这个模式实际上等于“无论值是什么,都把它绑定到变量 x”。
为了更清楚地看出 let 的模式匹配这一面,来看示例 19-1,它在 let 中使用模式来解构一个元组。
fn main() {
let (x, y, z) = (1, 2, 3);
}
示例 19-1: 使用模式解构元组,并一次创建三个变量
这里,我们让一个元组去匹配一个模式。Rust 会比较值 (1, 2, 3) 和模式 (x, y, z),并发现该值和模式匹配,也就是说,两边的元素个数相同;于是 Rust 将 1 绑定到 x,将 2 绑定到 y,将 3 绑定到 z。你可以把这个元组模式看作其中嵌套了三个独立的变量模式。
如果模式中的元素数量与元组中的元素数量不一致,那么整体类型就不会匹配,编译器也会报错。例如,示例 19-2 展示了试图用两个变量去解构一个三个元素的元组,这样是行不通的。
fn main() {
let (x, y) = (1, 2, 3);
}
示例 19-2: 错误地构造了一个模式,其变量数量与元组中的元素数量不匹配
尝试编译这段代码会得到如下类型错误:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
要修复这个错误,可以像本章后面 “忽略模式中的值” 一节中会看到的那样,使用 _ 或 .. 来忽略元组中的一个或多个值。如果问题在于模式里的变量太多,那么解决办法就是删掉一些变量,使变量数量与元组中的元素数量相等。
条件 if let 表达式
在第六章中,我们讨论过如何使用 if let 表达式,它主要是用来简写只匹配一种情况的 match。此外,if let 还可以有一个对应的 else,在 if let 中的模式不匹配时执行其中的代码。
示例 19-3 表明,我们也可以混合使用 if let、else if、else if let 和 else。与 match 表达式相比,这样做给了我们更多灵活性;在 match 中,我们只能表达“把一个值与若干模式比较”。另外,Rust 也不要求一连串 if let、else if 和 else if let 分支中的条件彼此相关。
示例 19-3 中的代码会根据一系列条件检查来决定背景色应该是什么。为了举例,我们创建了几个带硬编码值的变量;而在真实程序里,这些值可能来自用户输入。
文件名:src/main.rs
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {color}, as the background");
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
示例 19-3: 混合使用 if let、else if、else if let 和 else
如果用户指定了喜欢的颜色,就用该颜色作为背景色。如果没有指定喜欢的颜色,而且今天是星期二,那么背景色就是绿色。否则,如果用户把年龄作为字符串提供出来,并且我们能够成功将其解析为数字,那么背景色就会根据该数字的值变成紫色或橙色。如果这些条件都不满足,背景色就是蓝色。
这种条件结构让我们能够支持复杂的需求。使用这里的硬编码值,这个例子会打印 Using purple as the background color。
你还可以看到,if let 也能像 match 分支一样引入新变量,并遮蔽已有变量:if let Ok(age) = age 这一行引入了一个新的 age 变量,它保存 Ok 变体中的值,并遮蔽了原来的 age 变量。这意味着我们必须把 if age > 30 这个条件放在该代码块内部:不能把这两个条件合并成 if let Ok(age) = age && age > 30。因为我们想拿来和 30 比较的那个新 age,在由大括号开启的新作用域开始之前是无效的。
使用 if let 表达式的缺点是,编译器不会像检查 match 那样检查它的穷尽性。如果我们省略最后一个 else 块,从而漏掉对某些情况的处理,编译器也不会提醒我们这里可能存在逻辑错误。
while let 条件循环
与 if let 在结构上类似的是 while let 条件循环,它允许 while 循环在模式持续匹配期间一直运行。示例 19-4 展示了一个 while let 循环,它等待线程之间发送的消息,不过这里检查的是 Result,而不是 Option。
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for val in [1, 2, 3] {
tx.send(val).unwrap();
}
});
while let Ok(value) = rx.recv() {
println!("{value}");
}
}
示例 19-4: 使用 while let 循环,只要 rx.recv() 返回 Ok 就打印值
这个例子会打印 1、2,然后是 3。recv 方法会从信道的接收端取出第一条消息,并返回 Ok(value)。在第十六章第一次见到 recv 时,我们是直接对错误调用 unwrap,或者把它当作迭代器配合 for 循环使用。不过正如示例 19-4 所示,我们也可以使用 while let,因为只要发送端还存在,且不断有消息到达,recv 每次都会返回 Ok;当发送端断开连接后,它就会返回 Err。
for 循环
在 for 循环中,紧跟在 for 关键字后面的值就是一个模式。例如,在 for x in y 中,x 就是那个模式。示例 19-5 展示了如何在 for 循环中使用模式来解构一个元组。
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{value} is at index {index}");
}
}
示例 19-5: 在 for 循环中使用模式来解构一个元组
示例 19-5 中的代码会打印出如下内容:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2
我们使用 enumerate 方法适配了一个迭代器,使它产生由“值及其索引”组成的元组。它产生的第一个值是元组 (0, 'a')。当该值与模式 (index, value) 匹配时,index 会是 0,value 会是 'a',于是打印出输出的第一行。
函数参数
函数参数也可以是模式。示例 19-6 中的代码声明了一个名为 foo 的函数,它接收一个名为 x、类型为 i32 的参数;到现在为止,这种写法应该已经很熟悉了。
fn foo(x: i32) {
// code goes here
}
fn main() {}
示例 19-6: 在参数中使用模式的函数签名
x 这一部分就是一个模式!就像我们在 let 中做的那样,也可以在函数参数中用模式匹配一个元组。示例 19-7 展示了在向函数传参时如何拆开元组中的值。
文件名:src/main.rs
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
示例 19-7: 一个在参数中解构元组的函数
这段代码会打印 Current location: (3, 5)。值 &(3, 5) 会匹配模式 &(x, y),因此 x 的值是 3,y 的值是 5。
由于闭包与函数类似,正如第十三章所讨论的那样,我们也可以像在函数参数列表中那样,在闭包参数列表中使用模式。
现在我们已经看过很多使用模式的方式了,不过模式在各处并不总是以同样的方式工作;在某些位置,模式必须是不可反驳的(irrefutable),这意味着它们必须匹配所提供的任何值;而在另一些位置,它们则可以是可反驳的(refutable)。接下来让我们讨论这两个概念。
Refutability(可反驳性): 模式是否会匹配失效
可反驳性:模式是否可能匹配失败
模式分为两种形式:可反驳(refutable)和不可反驳(irrefutable)。对传入的任意可能值都能匹配的模式,称为不可反驳模式。比如语句 let x = 5; 中的 x 就是一个例子,因为 x 可以匹配任何值,因此不可能匹配失败。对某些可能值会匹配失败的模式,称为可反驳模式。比如表达式 if let Some(x) = a_value 中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么模式 Some(x) 就不会匹配。
函数参数、let 语句和 for 循环只能接受不可反驳模式,因为当值不匹配时,程序无法做出有意义的事情。if let 和 while let 表达式,以及 let...else 语句既接受可反驳模式,也接受不可反驳模式;不过编译器会对其中的不可反驳模式发出警告,因为根据定义,这些结构本来就是为处理可能失败的情况而设计的:条件判断的意义就在于它可以根据成功或失败执行不同的逻辑。
一般来说,你不必时刻担心可反驳模式和不可反驳模式的区别;不过你确实需要熟悉“可反驳性”这个概念,这样当你在错误信息里看到它时,就知道该如何应对。遇到这类情况时,你需要根据代码想要表达的行为,修改模式本身,或者修改与之搭配使用的语法结构。
让我们来看一个例子:当我们试图在 Rust 要求使用不可反驳模式的地方使用可反驳模式,以及反过来时,会发生什么。示例 19-8 展示了一个 let 语句,不过我们给它写了模式 Some(x),这是一个可反驳模式。正如你可能已经猜到的,这段代码不会通过编译。
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
示例 19-8: 尝试在 let 中使用可反驳模式
如果 some_option_value 的值是 None,它就无法匹配模式 Some(x),这说明该模式是可反驳的。然而,let 语句只能接受不可反驳模式,因为当值是 None 时,这段代码并没有什么合法的后续操作可做。在编译期,Rust 会抱怨我们试图在要求不可反驳模式的地方使用可反驳模式:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
因为我们没有覆盖模式 Some(x) 所对应的所有合法值(而且也不可能覆盖全部),所以 Rust 理所当然地给出了编译错误。
如果在某个需要不可反驳模式的地方却有一个可反驳模式,我们可以通过修改使用该模式的代码来修复这个问题:不用 let,而改用 let...else。这样一来,如果模式不匹配,大括号中的代码就会处理该值。示例 19-9 展示了如何修复示例 19-8 中的代码。
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value else {
return;
};
}
示例 19-9: 用 let...else 和一个带可反驳模式的代码块来代替 let
我们给这段代码留出了一条退路!现在这段代码是完全合法的。不过这也意味着,我们不能在这里使用不可反驳模式而不收到警告。如果我们给 let...else 一个总能匹配的模式,比如示例 19-10 中的 x,编译器就会发出警告。
fn main() {
let x = 5 else {
return;
};
}
示例 19-10: 尝试在 let...else 中使用不可反驳模式
Rust 会抱怨说,在 let...else 中使用不可反驳模式没有意义:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
--> src/main.rs:2:5
|
2 | let x = 5 else {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `else` clause is useless
= help: consider removing the `else` clause
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
基于这个原因,match 分支必须使用可反驳模式,只有最后一个分支例外,它应该用一个不可反驳模式来匹配所有剩余值。Rust 允许我们在一个只有单个分支的 match 中使用不可反驳模式,不过这种写法并没有太大用处,而且完全可以被更简单的 let 语句替代。
既然现在你已经知道模式该用在什么地方,以及可反驳模式和不可反驳模式之间的区别,接下来我们就来看看所有可以用来创建模式的语法。
模式语法
模式语法
在本节中,我们收集了模式中所有有效的语法,并讨论为什么以及何时你可能要使用这些语法。
匹配字面值
如第六章所示,可以直接匹配字面值模式。如下代码给出了一些例子:
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
这段代码会打印 one 因为 x 的值是 1。如果希望代码获得特定的具体值,则该语法很有用。
匹配命名变量
命名变量(Named variables)是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而,当在 match、if let 或 while let 表达式中使用命名变量时,会出现一些复杂情况。由于这些表达式会开始一个新作用域,作为模式一部分在表达式内部声明的变量会遮蔽外部同名变量,这与所有变量的遮蔽规则一致。在示例 19-11 中,声明了一个值为 Some(5) 的变量 x 和一个值为 10 的变量 y。接着在值 x 上创建了一个 match 表达式。观察匹配分支中的模式和结尾的 println!,并在运行此代码或进一步阅读之前推断这段代码会打印什么。
文件名:src/main.rs
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
示例 19-11: 一个 match 语句其中一个分支引入了遮蔽变量 y
让我们看看当 match 语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 x 中定义的值,所以代码继续执行。
第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为我们在 match 表达式的新作用域中,这是一个新变量,而不是开头声明为值 10 的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 x 中 Some 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched, y = 5。
如果 x 的值是 None 而不是 Some(5),头两个分支的模式不会匹配,所以会匹配下划线。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被遮蔽的 x。在这个假想的例子中,match 将会打印 Default case, x = None。
一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后的 println! 会打印 at the end: x = Some(5), y = 10。
为了创建能够比较外部 x 和 y 的值,又不引入新的变量去遮蔽已有 y 的 match 表达式,我们需要相应地使用带有条件的匹配守卫(match guard)。我们稍后将在 “使用匹配守卫添加额外条件” 这一小节讨论匹配守卫。
匹配多个模式
在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或(or)运算符模式。例如,如下代码将 x 的值与匹配分支相比较,第一个分支有或选项,意味着如果 x 的值匹配此分支的任一个值,它就会运行:
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
上面的代码会打印 one or two。
通过 ..= 匹配值范围
..= 语法允许你匹配一个闭区间范围(range)内的值。在如下代码中,当模式匹配任何在给定范围内的值时,该分支会执行:
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
如果 x 是 1、2、3、4 或 5,第一个分支就会匹配。这个语法在匹配多个值时相比使用 | 运算符来表达相同的意思更为方便;如果使用 | 则不得不指定 1 | 2 | 3 | 4 | 5。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候!
编译器会在编译时检查范围不为空,而 char 和数字值是 Rust 仅有的可以判断范围是否为空的类型,所以范围只允许用于数字或 char 值。
如下是一个使用 char 类型值范围的例子:
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust 知道 'c' 位于第一个模式的范围内,并会打印出 early ASCII letter。
解构并分解值
也可以使用模式来解构结构体、枚举和元组,以便使用这些值的不同部分。让我们来分别看一看。
结构体
示例 19-12 展示带有两个字段 x 和 y 的结构体 Point,可以通过带有模式的 let 语句将其分解:
文件名:src/main.rs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
示例 19-12: 解构一个结构体的字段为单独的变量
这段代码创建了变量 a 和 b 来匹配结构体 p 中的 x 和 y 字段。这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p; 包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。示例 19-13 展示了与示例 19-12 有着相同行为的代码,不过 let 模式创建的变量为 x 和 y 而不是 a 和 b:
文件名:src/main.rs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
示例 19-13: 使用结构体字段简写来解构结构体字段
这段代码创建了变量 x 和 y,与变量 p 中的 x 和 y 相匹配。其结果是变量 x 和 y 包含结构体 p 中的值。
也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。
示例 19-14 展示了一个 match 语句将 Point 值分成了三种情况:直接位于 x 轴上(此时 y = 0 为真)、位于 y 轴上(x = 0)或不在任何轴上的点。
文件名:src/main.rs
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
示例 19-14: 解构和匹配模式中的字面值
第一个分支通过指定字段 y 匹配字面值 0 来匹配任何位于 x 轴上的点。此模式仍然创建了变量 x 以便在分支的代码中使用。
类似的,第二个分支通过指定字段 x 匹配字面值 0 来匹配任何位于 y 轴上的点,并为字段 y 创建了变量 y。第三个分支没有指定任何字面值,所以其会匹配任何其他的 Point 并为 x 和 y 两个字段创建变量。
在这个例子中,值 p 因为其 x 包含 0 而匹配第二个分支,因此会打印出 On the y axis at 7。
记住 match 表达式一旦找到一个匹配的模式就会停止检查其它分支,所以即使 Point { x: 0, y: 0} 在 x 轴上也在 y 轴上,这些代码也只会打印 On the x axis at 0。
枚举
本书之前曾经解构过枚举(例如第六章示例 6-5),不过当时没有明确提到解构枚举的模式需要对应枚举所定义的储存数据的方式。让我们以示例 6-2 中的 Message 枚举为例,编写一个 match 使用模式解构每一个内部值,如示例 19-15 所示:
文件名:src/main.rs
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
示例 19-15: 解构包含不同类型值变体的枚举
这段代码会打印出 Change the color to red 0, green 160, and blue 255。尝试改变 msg 的值来观察其他分支代码的运行。
对于像 Message::Quit 这样没有任何数据的枚举变体,不能进一步解构其值。只能匹配其字面值 Message::Quit,因此模式中没有任何变量。
对于像 Message::Move 这样的类结构体枚举变体,可以采用类似于匹配结构体的模式。在变体名称后,使用大括号并列出字段变量以便将其分解以供此分支的代码使用。这里使用了示例 19-13 所展示的简写。
对于像 Message::Write 这样的包含一个元素,以及像 Message::ChangeColor 这样包含三个元素的类元组枚举变体,其模式则类似于用于解构元组的模式。模式中变量的数量必须与变体中元素的数量完全一致。
嵌套的结构体和枚举
目前为止,所有的例子都只匹配了深度为一级的结构体或枚举,不过当然也可以匹配嵌套的项!例如,我们可以重构示例 19-15 的代码在 ChangeColor 消息中同时支持 RGB 和 HSV 色彩模式,如示例 19-16 所示:
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
示例 19-16: 匹配嵌套的枚举
match 表达式第一个分支的模式匹配一个包含 Color::Rgb 枚举变体的 Message::ChangeColor 枚举变体,然后模式绑定了三个内部的 i32 值。第二个分支的模式也匹配一个 Message::ChangeColor 枚举变体,但是其内部的枚举会匹配 Color::Hsv 枚举变体。我们可以在一个 match 表达式中指定这些复杂条件,即使会涉及到两个枚举。
结构体和元组
甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
这将复杂的类型分解成部分组件以便可以单独使用我们感兴趣的值。
通过模式解构是一个方便将值的各个片段分离开来单独使用的方式,比如结构体中每个单独字段的值。
忽略模式中的值
有时忽略模式中的一些值是有用的,比如 match 中最后捕获全部情况的分支实际上没有做任何事,但是它确实负责匹配了所有剩余的可能值。有一些方法可以忽略模式中全部或部分值:使用 _ 模式(我们已经见过了),在另一个模式中使用 _ 模式,使用一个以下划线开始的名称,或者使用 .. 忽略所剩部分的值。让我们来分别探索如何以及为什么要这么做。
用 _ 忽略整个值
我们已经使用过下划线作为匹配但不绑定任何值的通配符模式了。虽然这作为 match 表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中,如示例 19-17 所示:
文件名:src/main.rs
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
示例 19-17: 在函数签名中使用 _
这段代码会完全忽略作为第一个参数传递的值 3,并会打印出 This code only uses the y parameter: 4。
大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时,当你需要特定类型签名但是函数实现并不需要某个参数时。这样可以避免一个存在未使用的函数参数的编译警告,就跟使用命名参数一样。
用嵌套的 _ 忽略部分值
也可以在一个模式内部使用_ 忽略部分值,例如,当只需要测试部分值但在期望运行的代码中没有用到其他部分时。示例 19-18 展示了负责管理设置值的代码。业务需求是用户不允许覆盖现有的自定义设置,但是可以取消设置,也可以在当前未设置时为其提供一个值。
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
示例 19-18: 当不需要 Some 中的值时在模式内使用下划线来匹配 Some 变体
这段代码会打印出 Can't overwrite an existing customized value 接着是 setting is Some(5)。在第一个匹配分支,我们不需要匹配或使用任一个 Some 变体中的值,但需要检测 setting_value 和 new_setting_value 是否均为 Some 变体。在这种情况下,我们打印出为何不改变 setting_value,并且不会改变它。
对于所有其他情况(setting_value 或 new_setting_value 任一为 None),这由第二个分支的 _ 模式体现,这时确实希望允许 new_setting_value 变为 setting_value。
也可以在一个模式中的多处使用下划线来忽略特定值,如示例 19-19 所示,这里忽略了一个五元元组中的第二和第四个值:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
示例 19-19: 忽略元组的多个部分
这会打印出 Some numbers: 2, 8, 32,值 4 和 16 会被忽略。
通过在变量名开头加 _ 来忽略未使用变量
如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为未使用的变量可能会是个 bug。但是有时创建一个还未使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头。示例 19-20 中创建了两个未使用变量,不过当编译代码时只会得到其中一个的警告:
文件名:src/main.rs
fn main() {
let _x = 5;
let y = 10;
}
示例 19-20: 以下划线开始变量名以便去掉未使用变量警告
这里得到了警告说未使用变量 y,不过没有警告说未使用 _x。
注意,只使用 _ 和使用以下划线开头的名称有些微妙的不同:比如 _x 仍会将值绑定到变量,而 _ 则完全不会绑定。为了展示这个区别的意义,示例 19-21 会产生一个错误。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
示例 19-21: 以下划线开头的未使用变量仍然会绑定值,它可能会获取值的所有权
我们会得到一个错误,因为 s 的值仍然会移动进 _s,并阻止我们再次使用 s。然而只使用下划线本身,并不会绑定值。示例 19-22 能够无错编译,因为 s 没有被移动进 _:
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
示例 19-22: 单独使用下划线不会绑定值
上面的代码能很好的运行;因为没有把 s 绑定到任何变量;它没有被移动。
用 .. 忽略值的剩余部分
对于有多个部分的值,可以使用 .. 语法来只使用特定部分并忽略其它值,从而避免不得不每一个忽略值列出下划线。.. 模式会忽略模式中剩余的任何没有显式匹配的值部分。在示例 19-23 中,有一个 Point 结构体存放了三维空间中的坐标。在 match 表达式中,我们希望只操作 x 坐标并忽略 y 和 z 字段的值:
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
示例 19-23: 通过使用 .. 来忽略 Point 中除 x 以外的字段
这里列出了 x 值,接着仅仅包含了 .. 模式。这比不得不列出 y: _ 和 z: _ 要来得简单,特别是在处理有很多字段的结构体,但只涉及一到两个字段时的情形。
.. 会扩展为所需要的值的数量。示例 19-24 展示了如何在元组中使用 ..:
文件名:src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
示例 19-24: 只匹配元组中的第一个和最后一个值并忽略掉所有其它值
这里用 first 和 last 来匹配第一个和最后一个值。.. 将匹配并忽略中间的所有值。
然而使用 .. 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。示例 19-25 展示了一个带有歧义的 .. 例子,因此其不能编译:
文件名:src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
示例 19-25: 尝试以有歧义的方式运用 ..
当编译这个示例时,会得到如下错误:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust 不可能决定在元组中匹配 second 值之前应该忽略多少个值,以及在之后忽略多少个值。这段代码可能表明我们意在忽略 2,绑定 second 为 4,接着忽略 8、16 和 32;抑或是意在忽略 2 和 4,绑定 second 为 8,接着忽略 16 和 32,以此类推。变量名 second 对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 .. 是有歧义的。
使用匹配守卫添加额外条件
匹配守卫(match guard)是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。但是注意,它们仅在 match 表达式中可用,不能用于 if let 或 while let 表达式。
这个条件可以使用模式中创建的变量。示例 19-26 展示了一个 match,其中第一个分支有模式 Some(x) 还有匹配守卫 if x % 2 == 0 (当 x 是偶数时为真):
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
示例 19-26: 在模式中加入匹配守卫
上例会打印出 The number 4 is even。当 num 与模式中第一个分支比较时,因为 Some(4) 匹配 Some(x) 所以可以匹配。接着匹配守卫检查 x 除以 2 的余数是否等于 0,因为它等于 0,所以第一个分支被选择。
相反如果 num 为 Some(5),因为 5 除以 2 的余数是 1 不等于 0 所以第一个分支的匹配守卫为 false。接着 Rust 会前往第二个分支,这次匹配因为它没有匹配守卫所以会匹配任何 Some 变体。
无法在模式中表达类似 if x % 2 == 0 的条件,所以通过匹配守卫提供了表达类似逻辑的能力。这种替代表达方式的缺点是,编译器不会尝试为包含匹配守卫的模式检查穷尽性。
在示例 19-11 中,我们提到可以使用匹配守卫来解决模式中变量遮蔽的问题,那里 match 表达式的模式中新建了一个变量而不是使用 match 之外的同名变量。新变量意味着不能够测试外部变量的值。示例 19-27 展示了如何使用匹配守卫修复这个问题。
文件名:src/main.rs
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
示例 19-27: 使用匹配守卫来测试与外部变量的相等性
现在这会打印出 Default case, x = Some(5)。现在第二个匹配分支中的模式不会引入一个遮蔽外部 y 的新变量 y,这意味着可以在匹配守卫中使用外部的 y。相比指定会遮蔽外部 y 的模式 Some(y),这里指定为 Some(n)。此新建的变量 n 并没有覆盖任何值,因为 match 外部没有变量 n。
匹配守卫 if n == y 并不是一个模式所以没有引入新变量。这个 y 正是外部的 y 而不是新的遮蔽变量 y,这样就可以通过比较 n 和 y 来表达寻找一个与外部 y 相同的值了。
也可以在匹配守卫中使用或运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。示例 19-28 展示了结合匹配守卫与使用了 | 的模式的优先级。这个例子中重要的部分是匹配守卫 if y 作用于 4、5 和 6,即使这看起来好像 if y 只作用于 6:
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
示例 19-28: 结合多个模式与匹配守卫
这个匹配条件表明此分支值匹配 x 值为 4、5 或 6 同时 y 为 true 的情况。运行这段代码时会发生的是第一个分支的模式因 x 为 4 而匹配,不过匹配守卫 if y 为 false,所以第一个分支不会被选择。代码移动到第二个分支,这会匹配,此程序会打印出 no。这是因为 if 条件作用于整个 4 | 5 | 6 模式,而不仅是最后的值 6。换句话说,匹配守卫与模式的优先级关系看起来像这样:
(4 | 5 | 6) if y => ...
而不是:
4 | 5 | (6 if y) => ...
运行代码后,优先级行为就很明显了:如果匹配守卫只作用于由 | 运算符指定的值列表的最后一个值,这个分支就会匹配且程序会打印出 yes。
使用 @ 绑定
at 运算符(@)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。示例 19-29 展示了一个例子,这里我们希望测试 Message::Hello 的 id 字段是否位于 3..=7 范围内,同时也希望能将其值绑定到 id_variable 变量中以便此分支相关联的代码可以使用它。可以将 id_variable 命名为 id,与字段同名,不过出于示例的目的这里选择了不同的名称。
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => {
println!("Found an id in range: {id_variable}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
示例 19-29: 使用 @ 在模式中绑定值的同时测试它
上例会打印出 Found an id in range: 5。通过在 3..=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。
第二个分支只在模式中指定了一个范围,分支相关代码没有一个包含 id 字段实际值的变量。id 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字段中的值,因为没有将 id 值保存进一个变量。
最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id。因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id 字段的值进行测试:任何值都会匹配该模式。
使用 @ 可以在一个模式中同时测试和保存变量值。
总结
模式是 Rust 中一个很有用的功能,它有助于我们区分不同类型的数据。当用于 match 语句时,Rust 确保模式会包含每一个可能的值,否则程序将不能编译。let 语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。
接下来,在本书倒数第二章中,我们将介绍一些 Rust 众多功能中较为高级的部分。