0%

Rust 权威指南(17):模式匹配

模式是 Rust 中一种用来匹配类型结构的特殊语法,将模式与 match 表达式或其他工具配合使用可以更好地控制程序流程。模式被用来与某个特定的值进行匹配。如果模式与值匹配成功,那么就可以在代码中使用这个值的某些部分。这篇文章将学习所有可以使用模式匹配的场景、不可失败模式与可失败模式之间的区别,以及代码中可能会出现的各种模式匹配语法。

所有可以使用模式的场合

match 分支

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

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

match 表达式必须穷尽匹配值的所有可能性。为了实现这一要求,可以在最后的分支使用全匹配模式。例如变量名可以被用来覆盖所有的可能性。另外 _ 模式也可以用来匹配所有可能的值,而且不会将它们绑定到任何一个变量上。

if let 表达式

if let 不仅可以当做只匹配单个分支的 match 表达式使用,而是可以混合使用 if letelse if 以及 else if let 进行匹配。这种混合语法可以灵活更多的灵活性,而且这些分支中的条件也可以彼此无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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, {}, as the background", color);
} 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")
}
}

和 match 分支类似,if let 分支能够以同样的方式对变量进行覆盖。但是与 match 表达式不同的是,if let 表达式的缺点是:它不会强制开发者穷尽值的所有可能性。

while let 条件循环

条件循环 while let 的构造与 if let 类似,它会反复执行同一个模式匹配直至出现失败的情形。如下是一个简单示例:

1
2
3
4
5
6
7
8
9
let mut stack = Vec::new();

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

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

for 循环

也可以在 for 循环内使用模式,for 语句中紧随关键字 for 的值就是一个模式,如下在 for 循环中使用模式来结构元祖:

1
2
3
4
5
let v = vec!['a', 'b', 'c'];

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

这里使用 enumerate() 方法来作为迭代器的适配器,它会在每次迭代过程中生成一个包含值本身以及值索引的元组。

let 语句

其实很多语句(甚至最基本的 let 语句)中也同样使用了模式。例如如下使用 let 来为变量赋值的语句:

1
let x = 5;

其实这个语句就用到了模式,更正式的 let 语句定义方法如下:

1
let PATTERN = EXPRESSION;

Rust 会将表达式与模式匹配,并为所有找到的名称赋值。所以上述赋值语句中,x 就是模式本身,而如下元祖结构语句则更清楚地说明了这一点:

1
let (x, y, z) = (1, 2, 3);

如果模式中元素的数量与元祖中元素的数量不同,这个类型就会匹配失败。如果需要忽略元祖中的某一个或多个值,那么可以使用 _ 或者 .. 语法

这里还要一种模式匹配语法需要注意,在变量前添加 &,它可以对引用进行解构。所以如下示例中,z 的类型是 i32。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}

fn main() {
let i = 32;
let j = &i;
let &z = j;

print_type_of(&i);
print_type_of(&j);
print_type_of(&z);
}
1
2
3
4
# cargo run
i32
&i32
i32

关于对引用的解构,更多的例子参考pointers/ref

函数的参数

函数的参数同样也是模式。与 let 语句类似,同样可以在函数参数中使用模式去匹配元祖。类似于函数的参数列表同样可以在闭包的参数列表中使用模式。

可失败性:模式是否会匹配失败

模式可以被分为不可失败(irrefutable)和可失败(refutable)两种类型。不可失败的模式能够匹配任何传入的值:

  • let x = 5; 中 x 就是不可失败模式
  • if let Some(x) = a 中的 Some(x) 就是可失败模式

函数参数、let 语句以及 for 循环只接受不可失败模式,我们程序无法在值不匹配时执行任何有意义的行为。if letwhile let 表达式则只接受可失败模式,因为它们在设计时就将匹配失败的情形考虑在内了:条件表达式的功能就是根据条件的成功与否执行不同的操作。

在 match 表达式中,除了最后一个,其他必须全部使用可失败模式,而最后的分支则应该使用不可失败模式,因为它需要匹配值的所有剩余情形。Rust 允许你在仅有一个分支的 match 表达式中使用不可失败模式,但这种语法几乎没有任何用处,它可以被简单的 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
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 = {:?}, y = {:?}", x, y);
}

匹配分支中引入了新的变量 y,它会匹配 Some 变体中携带的任意值,所以打印的不是最外层的变量 y,而是 match 表达式新引入的变量 y。如果你希望在 match 表达式中比较外部的 x 与 y,而不是引入新的变量,那么需要使用带有条件的 匹配守卫,后文会详细介绍。

多重模式

可以在 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"),
}
}

使用 ... 来匹配值区间

可以使用 ... 来匹配闭区间的值,范围模式只被允许使用数值或 char 值来进行定义,因为编译器需要在编译时确保范围的区间不为空,而 char 和数值则是 Rust 仅有的可以判断区间是否为空的类型。

1
2
3
4
5
6
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} = pRust 允许我们在模式匹配分解结构体字段时采用一种较为简单的写法:只需要列出结构体字段中的名称,模式就会自动创建出拥有相同名称的变量,所以可以简化为这种写法 let Point{x, y} = p

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

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),
}
}

接下来再来查看解构枚举的操作,有一个细节尤其需要注意:用于结构枚举的模式必须对应枚举定义中存储数据的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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{} and in the y direction {}", x, y);
},

Message::Write(text) => {
println!("Text message: {}", text);
},

Message::ChangeColor(r, g, b) => {
println!("Change color to red {}, green {} and blue {}", r, g, b);
},
}
}
  • 对于类似于结构体的枚举变体而言,可以采用类似于匹配结构体的模式
  • 对于类似于元祖的枚举体,使用类似于元祖匹配的模式

匹配语法还可以用于嵌套的结构中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 the color to red {}, green {}, and blue {}", r, g, b)
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change the color to hue {}, saturation {}, and value {}", h, s, v)
}
_ => ()
}
}

我们甚至可以按照某种更为复杂的方式来将模式混合、匹配或嵌套在一起。如下是一个示例:

1
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 use the y parameter: {}", y);
}

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

我们也可以在另一个模式中使用 _ 来忽略值的某些部分。当我们需要检查值的某一部分且不会用到其他部分时,就可以使用这一模式。

1
2
3
4
5
6
7
8
9
10
11
let mut setting_value = Some(5);
let mut 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;
}
}

类似地,可以在一个模式中多次使用下划线来忽略特定的值:

1
2
3
4
5
6
let numbers = (1, 2, 3, 4, 5);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
},
}

Rust 会在创建出一个变量却又没有过它时给出相应的告警,为了避免 Rust 在这些场景中因为某些未使用过的变量而抛出警告,可以在这些变量名称前添加 _

1
2
3
4
fn main() {
let _x = 5;
let y = 10;
}

有一个细节值得注意,使用下划线开头的变量名与仅仅使用 _ 作为变量名存在一个细微差别:_x 语法仍然将值绑定到变量上,而 _ 则完全不会进行绑定。所以如下代码会出现编译错误:

1
2
3
4
5
6
7
let s = Some(String::from("hello"));

if let Some(_s) = s {
println!("found a string");
}

println!("{:?}", s);

而上述代码如果仅仅使用下划线本身,则不会发生任何绑定操作,所以下面代码可以编译通过:

1
2
3
4
5
6
7
let s = Some(String::from("Hello!"));

if let Some(_) = s {
println!("found a string");
}

println!("{:?}", s);

对于拥有多个部分的值,可以使用语法 .. 来使用其中的某一部分并忽略剩余的部分。这使得我们不必为每个需要忽略的值都添加对应的 _ 模式来进行占位。.. 模式可以忽略一个值中没有被我们显式匹配的那些部分。

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

.. 语法会自动展开并填充任意多个所需的值。如下匹配一个元组:

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

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

注意使用 .. 不能出现任何歧义,如果模式中需要匹配的值或需要忽略的值是无法确定的,那么 Rust 就会产生一个编译时错误。

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

匹配守卫(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 => (),
}
}

这里首先 Some(4) 与 Some(x) 匹配,随后的匹配守卫则会检查模式中创建的变量是否小于 5。由于 num 同时满足这一条件,所以执行第一个分支。我们无法通过模式表达式出类似于 if x < 5 这样的条件,匹配守卫增强了语句中表达相关逻辑的能力。

匹配守卫也可以用来解决模式中变量覆盖的问题。如下是之前例子的修复版本:

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

匹配守卫 if n == y 不是一个模式,所以它不会引入新的变量,所以它访问的就是外部变量 y。

我们同样可以在匹配守卫中使用或运算符 | 来指定多重模式。注意,如下代码中 if y 是对于 4 | 5 | 6 起作用的:

1
2
3
4
5
6
7
let x = 4;
let y = false;

match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}

@ 绑定

@ 运算符允许我们测试一个值是否匹配模式的同时,创建存储该值的变量。如下是一个示例:

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

这里第一个分支希望测试 Message::Hello 的 id 字段是否在区间 3..=7 中,同时将这个变量的值绑定到变量 id_variable 上。第二个模式没有仅仅在模式中指定了值的区间,所以在该模式分支中的代码无法使用 id 字段中的值。最后一个分支则指定了没有区间约束的变量,该变量随后可以用于分支代码。