18.3 模式(匹配)的语法
18.3.1. 匹配字面值
模式可以直接匹配字面值。看个例子:
#![allow(unused)]
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
这段代码会打印 one,因为 x 中的值是 1。当你希望代码针对某个特定值采取行动时,这种语法非常有用。
18.3.2. 匹配命名变量
命名变量是可以匹配任意值的无可辩驳模式。看个例子:
#![allow(unused)]
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}");
}
这个例子的逻辑很简单;关键在于这里出现了两个名为 y 的名字。它们互不相关,分别处于不同的作用域。let y = 10 中的 y 用来存储 10,而 Some(y) 中的 y 用来提取 Option 类型的 Some 变体所携带的数据。
match 中的执行逻辑如下:
-
第一个分支中的模式与
x的值不匹配,因此继续往下执行。 -
第二个分支中的模式引入了一个名为
y的新变量,它会匹配Some内的任意值。因为我们处于match表达式内的新作用域,所以这是一个新的y变量,而不是开头那个值为 10 的y。这个新的y绑定会匹配Some内的任意值,而我们在x里恰好就有这样的值。因此,这个新的y会绑定到x中Some的内部值。该值是5,于是该分支的表达式会执行,并打印Matched, y = 5。 -
如果
x是None而不是Some(5)——当然在这个例子里不可能发生——那么前两个分支中的模式都不会匹配,于是会匹配到_。我们没有在通配符分支中引入x变量,所以表达式里的x仍然是未被遮蔽的外部x。在那种假设情况下,match会打印Default case, x = None。
输出:
Matched, y = 5
at the end: x = Some(5), y = 10
18.3.3. 多重模式
在 match 表达式里,可以使用管道符 | 语法(意思是或)来匹配多种模式。看个例子:
#![allow(unused)]
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
例子中的第一个分支会在 x 为 1 或 2 时匹配。
18.3.4. 使用 ..= 来匹配某个范围的值
看个例子:
#![allow(unused)]
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
这个例子的第一个分支表示:当 x 是从 1 到 5(含两端)的任意值——也就是 1、2、3、4 或 5——时都会匹配。
因为 Rust 能够判断范围是否为空的唯一类型是 char 和数值类型,所以范围只允许用于数字或 char 值。看个例子:
#![allow(unused)]
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
这个例子的第一个分支匹配从 a 到 j 的字符,第二个分支匹配从 k 到 z 的字符。
18.3.5. 解构以分解值
我们可以使用模式来解构 struct、enum 和元组,从而引用这些类型值的不同部分。
解构 struct
看个例子:
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);
}
Point结构体有两个字段x和y,类型都是i32。- 有一个名为
p的Point实例,其x字段为 0,y字段为 7。 - 然后我们用模式解构
p,把x的值绑定到a,把y的值绑定到b。
这么写还是有些冗长。如果把变量名 a 改成 x,把 b 改成 y,就可以简写成这样:
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);
}
解构还可以灵活地使用。看个例子:
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})");
}
}
}
- 第一个分支要求
x字段为任意值,y字段为 0。 - 第二个分支要求
x字段为 0,y字段为任意值。 - 第三个分支对
x和y的值没有任何限制。
解构 enum
看个例子:
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 the color to red {r}, green {g}, and blue {b}")
}
}
}
这段代码会打印 Change the color to red 0, green 160, and blue 255。
解构嵌套的 struct 和 enum
看个例子:
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}")
}
_ => (),
}
}
Message 的 ChangeColor 变体所携带的数据就是 Color 枚举。使用 match 表达式时,一层一层匹配即可。在 match 的前两个分支中,外层都是 ChangeColor 变体,内层分别对应 Color 的两个变体;里面的值都可以通过变量提取出来。
解构 struct 和元组
看个例子:
struct Point {
x: i32,
y: i32,
}
fn main() {
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
main 中模式的外层是一个有两个元素的元组:
- 第一个元素本身又是一个有两个元素的元组。
- 第二个元素是一个
Point结构体。
在模式中忽略值
有几种方式可以在模式中忽略整个值或部分值:
_:忽略整个值_配合其他模式:忽略部分值- 使用以
_开头的名称 ..:忽略值的剩余部分
使用 _ 来忽略整个值
看个例子:
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
这段代码会完全忽略作为第一个参数传入的值 3,并打印 This code only uses the y parameter: 4。
使用嵌套的 _ 来忽略值的一部分
看个例子:
#![allow(unused)]
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:?}");
}
这段代码会打印 Can't overwrite an existing customized value,然后打印 setting is Some(5)。在第一个分支中,我们不需要匹配或使用 Some 变体里的值,但我们确实需要确认 setting_value 和 new_setting_value 都是 Some 变体。这就是忽略值的一部分的含义。
第二个分支表示在所有其他情况下——如果 setting_value 或 new_setting_value 是 None——就把 new_setting_value 赋给 setting_value。这就是把 _ 与其他模式配合使用来忽略值的例子。
我们还可以在同一个模式的多个位置使用下划线,来忽略特定值。看个例子:
#![allow(unused)]
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
}
这里忽略了元组的第 2 个和第 4 个元素。这段代码会打印 Some numbers: 2, 8, 32,而值 4 和 16 会被忽略。
使用以 _ 开头的名称来忽略未使用的变量
看个例子:
fn main() {
let _x = 5;
let y = 10;
}
正常情况下,如果你创建了变量却没有使用它,Rust 编译器会发出警告。这里 _x 和 y 都没有被使用,但对 y 会有警告。那是因为 _x 以 _ 开头,告诉编译器这是一个临时变量。
请注意:只使用 _ 和使用以下划线开头的名称之间存在细微差别。语法 _x 仍然会把值绑定到变量,而 _ 根本不会绑定任何东西。看个例子:
#![allow(unused)]
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
我们会收到一个错误,因为 s 的值仍然会被移动到 _s 中,这会阻止我们打印 s。
在这种情况下,应该使用 _ 来避免绑定值:
#![allow(unused)]
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
使用 .. 来忽略值的剩余部分
看个例子:
struct Point {
x: i32,
y: i32,
z: i32,
}
fn main() {
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
使用 match 匹配时,我们只需要 x 字段,所以模式只写 x,其余部分用 .. 覆盖。
这么使用 .. 也是可以的:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
这只会取第一个和最后一个值,并忽略其余部分。
这么写 .. 是不行的:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
这里前面有 ..,后面也有 ..,而我们想要中间的元素。但具体是哪个元素呢?这么写时,编译器不知道 .. 应该跳过多少个元素,因此也不知道 second 指的是哪个元素。
输出:
$ cargo run
Compiling patterns v0.1.0 (/tmp/ch18-refresh/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
18.3.6. 使用 match guards 来提供额外条件
match guards(match 守卫)是 match 分支模式后面附加的 if 条件。分支要匹配,这个条件也必须满足。match guards 适用于比单纯模式更复杂的场景。
看个例子:
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 => (),
}
}
在 match 的第一个分支中,Some(x) 是模式,而 if x % 2 == 0 就是 match guard,它要求 Some 所携带的数据能被 2 整除。
无法在模式本身中表达 if x % 2 == 0 这个条件,因此 match guards 让我们能够表达这种逻辑。这种额外表达能力的缺点是:一旦涉及 match guard,编译器就不会再尝试检查穷尽性。
看第二个例子:
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}");
}
这段代码现在会打印 Default case, x = Some(5)。
match 守卫 if n == y 不是模式,因此不会引入新变量。这个 y 是外部的 y(值为 10),而不是新的遮蔽变量 y。我们可以通过比较,找出与外部 y 具有相同值的那些 n。
看第三个例子:
#![allow(unused)]
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
这个例子把 match 守卫与多重模式一起使用。
匹配条件规定:只有当 x 为 4、5 或 6,并且 y 为 true 时,该分支才会匹配。运行这段代码时,x 是 4,但 match guard 中的 y 为 false,所以第一个分支不会执行,第二个分支会打印 no。
这里需要注意的是模式相对于 match 守卫的优先级。它应该是:
#![allow(unused)]
fn main() {
(4 | 5 | 6) if y => ...
}
而不是:
#![allow(unused)]
fn main() {
4 | 5 | (6 if y) => ...
}
18.3.7. @ 绑定
@ 符号让我们可以创建一个变量,该变量可以在测试某个值是否与模式匹配的同时保存该值。
看个例子:
enum Message {
Hello { id: i32 },
}
fn main() {
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}"),
}
}
在这个 match 的第一个分支中,id 字段的值被绑定到 id_variable,同时还会检查它是否落在从 3 到 7(含两端)的闭区间内。