The Rust programming language 读书笔记——模式匹配

模式是 Rust 中一种用来匹配类型结构的特殊语法,将其与 match 表达式或其他工具配合使用可以更好地控制程序流程。
模式被用来与某个特定的值进行匹配,若匹配成功,则可以继续使用这个值的某些部分;若匹配失败,模式对应的代码就被简单地略过。

模式的应用场景

match 分支

模式可以被应用在 match 表达式的分支中。
match 表达式由 match 关键字、待匹配的值以及至少一个匹配分支组成。匹配分支则由某个模式及模式匹配成功后应当执行的表达式组成。

1
2
3
4
5
match 值 { 
模式 => 表达式,
模式 => 表达式,
模式 => 表达式,
}

match 表达式必须穷尽匹配值的所有可能性。为了确保代码满足要求,可以在最后的分支处使用全匹配模式。例如变量名可以被用来覆盖所有剩余的可能性。
还有一个特殊的 _ 模式可以被用来匹配所有可能的值,且不将它们绑定到任何一个变量上。即忽略所有未被指定的值。

1
2
3
4
5
6
7
8
9
10
fn main() {
let some_value = 3;
match some_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
}

if let 表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let favorite_color: Option<&str> = None;
let age: Result<u8, _> = "34".parse();

if let Some(color) = favorite_color {
println!("Using your favorite color {} as the background", color);
} 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");
}
}

上述代码通过执行一系列的条件检查来确定使用的背景颜色。其中的变量已经被赋予了硬编码值,但现实中应当通过用户输入来获取这些值。

和 match 分支类似,if let 分支能够以同样的方式对变量进行覆盖。if let Ok(age) = age 这句代码中引入了新的变量 age 来存储 Ok 变体中的值,并覆盖了右侧的同名变量。

while let 循环

while let 会反复执行同一个模式匹配直到出现失败的情形。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
println!("{}", top);
}
}

上面的代码会依次打印 3、2、1。其中的 pop 方法会尝试取出动态数组的最后一个元素并将它包裹在 Some(value) 中返回。若动态数组为空,则 pop 返回 None。while 循环会在 pop 返回 Some 时执行循环体中的代码,pop 返回 None 时结束循环。

for 循环

for 语句中紧随关键字 for 之后的值就是一个模式。比如 for x in y 中的 x 就是一个模式。

在 for 循环中使用模式来解构元组:

1
2
3
4
5
6
7
fn main() {
let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
}

上述代码使用 enumerate 方法作为迭代器的适配器,会在每次迭代过程中生成一个包含值本身及其索引的元组。如首次调用 enumerate 会产生元组 (0, 'a')。当这个值与模式 (index, value) 进行匹配时,index 就会被赋值为 0,value 就会被赋值为 ‘a’。

let 语句

最基本的 let 赋值语句中也同样用到了模式。更正式的 let 语句的定义如下:
let PATTERN = EXPRESSION;

在类似于 let x = 5; 这样的语句中,单独的变量名成为最朴素的模式。其中 x 作为模式表达的含义是,将此处匹配到的所有内容绑定至变量 x,因为 x 就是整个模式本身。

用 let 模式匹配来解构元组:
let (x, y, z) = (1, 2, 3);

如果模式中元素的数量与元组中元素的数量不同,则整个类型会匹配失败,导致编译错误。

函数的参数

函数的参数同样也是模式。

1
2
3
4
5
6
7
8
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}

fn main() {
let point = (3, 5);
print_coordinates(&point);
}

模式 &(x, y) 能够和值 &(3, 5) 匹配,因此 x 的值为 3,y 的值为 5。

可失败性

模式可以被分为不可失败(irrefutable)和可失败(refutable)两种类型
不可失败的模式能够匹配任何传入的值。如语句 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 表达式则只接收可失败模式。因为它们在被设计时就将匹配失败的情形考虑在内了,条件表达式的功能就是根据条件的成功与否执行不同的操作。

模式语法

匹配字面量
1
2
3
4
5
6
7
8
9
10
fn main() {
let x = 1;

match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
匹配命名变量

命名变量是一种可以匹配任何值的不可失败模式。需要注意的是,当我们在 match 表达式中使用命名变量时,由于 match 开启了一个新的作用域,所以被定义在 match 表达式内作为模式一部分的变量会覆盖掉 match 结构外的同名变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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),
}
// => Matched, y = 5

println!("at the end: x = {:?}, y = {:?}", x, y);
// => at the end: x = Some(5), y = 10
}

在上述代码中,第二个匹配分支的模式引入了新的变量 y,它会匹配 Some 变体中携带的任何值。因为处在 match 表达式创建的新作用域中,这里的 y 是一个新的变量,而不是程序起始处声明的那个存储了 10 的 y。
新的 y 绑定能够匹配 Some 中的任意值,即匹配 x 变量中 Some 内部的值 5。

match 表达式创建的作用域会随着当前表达式的结束而结束,其内部的 y 也无法幸免。因此代码最后的 println! 会输出 at the end: x = Some(5), y = 10

多重模式

可以在 match 表达式的分支匹配中使用 | 来表示或的意思,从而一次性地匹配多个模式。

1
2
3
4
5
6
7
8
9
fn main() {
let x = 1;

match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}

使用 ..= 来匹配区间
1
2
3
4
5
6
7
8
fn main() {
let x = 5;

match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
使用解构来分解值

可以使用模式来分解结构体、枚举、元组或引用,从而使用这些值中的不同部分。

解构结构体

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

上述代码创建了 a 和 b 两个变量,分别匹配了 p 结构体中字段 x 和 y 的值。
采用与字段名相同的变量名在实践中非常常见,为了避免写出类似于 let Point { x: x, y: y } = p 这样冗余的代码,Rust 允许采用如下形式的代码解构结构体:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

除了为所有字段创建变量,还可以在结构体模式中使用字面量来进行解构。这一技术使我们可以在某些特定字段符合要求的前提下再对其他字段进行解构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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),
}
}

通过在第一个分支中要求 y 字段匹配字面量 0,从而匹配到所有位于 x 轴上的点,同时创建了一个可以在随后代码块中使用的 x 变量。
类似的第二个分支匹配 y 轴上的点,第三个分支匹配所有剩余的点。

甚至可以按照某种更为复杂的方式来将模式混合、匹配或嵌套在一起。
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

忽略模式中的值

使用 _ 忽略整个值
可以使用下划线 _ 作为通配符来匹配任意可能的值而不绑定值本身。虽然 _ 模式最常被用在 match 表达式的最后一个分支中,实际上我们可以把它用于包括函数参数在内的一切模式中。

1
2
3
4
5
6
7
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}

fn main() {
foo(3, 4);
}

上述代码会忽略传给第一个参数的值 3。忽略函数参数在某些情况下会变得有用。比如正在实现一个 trait,而这个 trait 的方法包含了你不需要的某些参数。此时就可以借助忽略模式避免编译器产生未使用变量的警告。

使用 .. 忽略值的剩余部分

1
2
3
4
5
6
7
8
9
10
11
12
13
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),
}
}

.. 语法会自动展开并填充任意多个所需的值。

1
2
3
4
5
6
7
8
9
fn main() {
let numbers = (2, 4, 8, 16, 32);

match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
}
}
}

上述代码使用 firstlast 分别匹配了元组中的第一个值和最后一个值,而它们之间的 .. 模式则会匹配并忽略中间的值。

使用匹配守卫添加额外条件

匹配守卫(match guard)是附加在 match 分支模式后的 if 条件语句,分支中的模式只有在该条件被同时满足时才能匹配成功。
匹配守卫的条件可以使用模式中创建的变量。

1
2
3
4
5
6
7
8
9
fn main() {
let num = Some(4);

match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
}

上述代码中,num 能够与第一个分支中的模式匹配成功,随后的匹配守卫则会检查模式中创建的变量 x 是否小于 5。由于 num 同样满足这一条件,最终执行了第一个分支中的代码。
假设 num 的值是 Some(10),则第一个匹配分支中的匹配守卫无法成立,Rust 会进入第二个分支继续比较最终匹配成功。

我们无法通过模式表达类似于 if x < 5 这样的条件,匹配守卫增强了语句中表达相关逻辑的能力。

参考资料

The Rust Programming Language