Rust的模式匹配详解

Rust中经常使用到的一个功能是模式匹配,例如let变量赋值本质上就是模式匹配。

官方手册参考:https://doc.rust-lang.org/reference/patterns.html

模式匹配的使用场景

可在如下几种情况下使用模式匹配:

  • let变量赋值
  • 函数参数传值时的模式匹配
  • match分支
  • if let
  • while let
  • for循环的模式匹配

let变量赋值时的模式匹配

let变量赋值时的模式匹配:

1
let PATTERN = EXPRESSION;

变量是一种最简单的模式,变量名位于Patter位置,赋值时的过程:将表达式与模式进行比较匹配,并将任何找到的变量名进行赋值

例如:

1
2
let x = 5;
let (x,y) = (1,2);

如果模式中的元素数量和表达式中返回的元素数量,则匹配失败,编译将无法通过。

1
let (x,y,z) = (1,2);  // 失败

函数参数传值时的模式匹配

为函数参数传值和使用let变量赋值是类似的,本质都是在做模式匹配的操作。

例如:

1
2
3
4
5
6
7
fn f1(i: i32){
// xxx
}

fn f2(&(x,y): &(i32,i32)){
// yyy
}

函数f1的参数i就是模式,当调用f1(88)时,88是表达式,将赋值给找到的变量名i。

函数f2的参数&(x,y)是模式,调用f2( &(2,8) )时,将表达式&(2,8)与模式&(x,y)进行匹配,并为找到的变量名x和y进行赋值:x=2,y=8

match分支

match分支匹配的用法非常灵活。它的语法为:

1
2
3
4
5
match VALUE {
PATTERN1 => EXPRESSION1,
PATTERN2 => EXPRESSION2,
PATTERN3 => EXPRESSION3,
}

例如,可以使用match来穷举枚举类型的所有成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Device {
Laptop,
Desktop,
Phone,
Pad,
}
fn main(){
match Device::Desktop {
Device::Laptop => 1,
Device::Desktop => 2,
Device::Phone => 3,
_ => 4,
}
}

使用match时,要求穷尽所有可能的情况,如果有遗漏的情况,编译将失败。

可以将_作为最后一个分支的PATTERN,它将匹配剩余所有情况。正如上面的示例。

另外,match自身也是表达式,它可以赋值给某个变量。

1
2
3
4
5
6
let x = match Device::Desktop {
Device::Laptop => 1,
Device::Desktop => 2,
Device::Phone => 3,
_ => 4,
};

if let

if let是match的一种特殊情况的语法糖:当只关心一个match分支,其余情况全部由_负责匹配时,可以将其改写为更精简if let语法。

1
2
3
if let PATTERN = EXPRESSION {
// xxx
}

这表示将EXPRESSION的返回值与PATTERN模式进行匹配,如果匹配成功,则为找到的变量进行赋值,这些变量在大括号作用域内有效。如果匹配失败,则不执行大括号中的代码。

例如:

1
2
3
4
5
6
7
8
9
10
11
let u8_value = Some(5_u8);
if let Some(5) = u8_value { // 匹配了但是没有找到变量
println!("five");
}

// 等价于如下代码
let u8_value = Some(5_u8);
match u8_value {
Some(5) => println!("five"),
_ => (),
}

if let可以结合else ifelse if letelse一起使用。

1
2
3
4
5
6
7
8
9
if let PATTERN = EXPRESSION {
// XXX
} else if {
// YYY
} else if let PATTERN = EXPRESSION {
// zzz
} else {
// zzzzz
}

这时候它们和match多分支类似。但实际上有很大的不同,使用match分支匹配时,要求分支之间是关联的(例如枚举的各个成员)且穷尽的,但Rust编译器不会检查if let的模式之间是否有关联关系,也不检查if let是否穷尽所有可能情况,因此,即使在逻辑上有错误,Rust也不会给出编译错误提醒。

例如,《The Rust Programming Language》给出了一个示例:

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

while let

只要while let的模式匹配成功,就会一直执行while循环内的代码。

例如:

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

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

stack.pop成功时,将匹配Some(top)成功,并将pop的值赋值给top,当没有元素可pop时,返回None,匹配失败,于是while循环退出。

for循环

这个无需解释。一个示例即可:

1
2
3
4
let v = vec![];
for (idx, value) in v.iter().enumerate(){
println!("{}: {}", idx, value);
}

模式的两种形式:refutable和irrefutable

从前面介绍的几种模式匹配可了解到,模式匹配的方式不唯一,有的时候是一定匹配成功的变量赋值型(let/for/函数传参)模式匹配,有的时候是可能匹配失败的模式匹配。

Rust中为这两种定义了专门的称呼:

  • 不可反驳的模式(irrefutable):一定会匹配成功,否则编译错误
  • 可反驳的的模式(refutable):可以匹配成功,也可以匹配失败,匹配失败的结果是不执行对应分支的代码

当模式匹配处使用了不接受的模式时,将会编译错误或给出警告。

1
2
3
4
5
6
7
// let变量赋值时使用可反驳的模式(允许匹配失败),编译失败
let Some(x) = some_value;

// if let处使用了不可反驳模式,没有意义(一定会匹配成功),给出警告
if let x = 5 {
// xxx
}

对于match来说,有以下几个示例可说明它的使用方式:

1
2
3
4
5
6
7
8
9
10
match value {
Some(5) => (), // 允许匹配失败,是可反驳模式
Some(50) => (),
_ => (), // 一定会匹配成功,是不可反驳模式
}

match value {
x => println!("{}", x), // 当只有一个Pattern分支时,可以是不可反驳模式
_ => (),
}

完整的模式语法

字面量模式

模式部分可以是字面量:

1
2
3
4
5
6
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("anything"),
}

模式带有变量名

例如:

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(y) => println!("Matched, y = {:?}", y), // 匹配成功,输出5
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y); // 输出10
}

上面的match会匹配第二个分支,同时为找到的变量y进行赋值,即y=5。这个y只在第二个分支对应的代码部分有效,跳出作用域后,y恢复为y=10

多选一模式

使用|可组合多个模式,表示逻辑或(or)的意思。

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

范围模式

Rust支持数值和字符的范围,有如下几种范围表达式:

Production Syntax Type Range
RangeExpr start..end std::ops::Range start ≤ x < end
RangeFromExpr start.. std::ops::RangeFrom start ≤ x
RangeToExpr ..end std::ops::RangeTo x < end
RangeFullExpr .. std::ops::RangeFull -
RangeInclusiveExpr start..=end std::ops::RangeInclusive start ≤ x ≤ end
RangeToInclusiveExpr ..=end std::ops::RangeToInclusive x ≤ end

但范围作为模式时,只允许全闭合的..=范围,其他类型的范围都会报错。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 数值范围
let x = 79;
match x {
0..=59 => println!("不及格"),
60..=89 => println!("良好"),
90..=100 => println!("优秀"),
_ => println!("error"),
}

// 字符范围
let y = 'c';
match y {
'a'..='j' => println!("a..j"),
'k'..='z' => println!("k..z"),
_ => (),
}

模式解构赋值

模式可用于解构赋值,可解构的类型包括struct、enum、tuple以及它们的引用。

解构赋值时,可使用_作为某个变量的占位符,使用..作为剩余所有变量的占位符(使用..时不能产生歧义,例如(..,x,..)是有歧义的)。当解构的类型包含了命名字段时,可使用filedname简化fieldname: fieldname的书写。

解构struct

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
struct Point2 {
x: i32,
y: i32,
}

struct Point3 {
x: i32,
y: i32,
z: i32,
}

fn main(){
let p = Point2{x: 0, y: 7};

// 等价于 let Point2{x: x, y: y} = p;
let Point2{x, y} = p;
// 解构时可修改变量名: let Point2{x: a, y: b} = p;
println!("x: {}, y: {}", x, y);

let ori = Point{x: 0, y: 0, z: 0};
match origin{
// 使用..忽略解构后剩余的值
Point3 {x, ..} => println!("{}", x),
}
}

解构enum

1
2
3
4
5
6
7
8
9
10
11
12
enum IPAddr {
IPAddr4(u8,u8,u8,u8),
IPAddr6(String),
}
fn main(){
let ipv4 = IPAddr::IPAddr4(127,0,0,1);
match ipv4 {
// 丢弃解构后的第四个值
IPAddr(a,b,c,_) => println!("{},{},{}", a,b,c),
IPAddr(s) => println!("{}", s),
}
}

解构元组

1
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

@绑定变量名

当解构后进行模式匹配时,如果某个值没有对应的变量名,则可以使用@手动绑定一个变量名。

例如:

1
2
3
4
5
6
struct S(i32, i32);

match S(1, 2) {
S(z @ 1, _) | S(_, z @ 2) => assert_eq!(z, 1),
_ => panic!(),
}

再例如:

1
2
3
4
5
6
match slice {
[.., "!"] => println!("!!!"),
[start @ .., "z"] => println!("starts with: {:?}", start),
["a", end @ ..] => println!("ends with: {:?}", end),
rest => println!("{:?}", rest),
}

ref和mut修饰模式中的变量

当进行解构赋值时,很可能会将变量拥有的所有权转移出去,从而使得原始变量变得不完整或直接失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person{
name: String,
age: i32,
}

fn main(){
let p = Person{name: String::from("junmajinlong"), age: 23};
let Person{name, age} = p;

println!("{}", name);
println!("{}", age);
println!("{}", p.name); // 错误,name字段所有权已转移
}

如果想要在解构赋值时不丢失所有权,有以下几种方式:

在模式中使用ref修改变量名相当于在被解构值上加&符号表示引用。

1
2
3
4
let x = 5_i32;         // x的类型:i32
let x = &5_i32; // x的类型:&i32
let ref x = 5_i32; // x的类型:&i32
let ref x = &5_i32; // x的类型:&&i32

因此,使用ref修饰了模式中的变量名后,对应值的所有权就不会发生转移,而是只读借用给该变量。

如果想要对解构赋值的变量具有数据的修改权,需要使用mut关键字修饰模式中的变量,但这样会转移原值的所有权,此时可不要求原变量是可变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug)]
struct Person {
name: String,
age: i32,
}

fn main() {
let p = Person {
name: String::from("junma"),
age: 23,
};
match p {
Person { mut name, age } => {
name.push_str("jinlong");
println!("name: {}, age: {}", name, age)
},
}
//println!("{:?}", p); // 错误
}

如果不想在可修改数据时丢失所有权,可在mut的基础上加上ref关键字,就像&mut xxx一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[derive(Debug)]
struct Person {
name: String,
age: i32,
}

fn main() {
let mut p = Person { // 这里要改为mut p
name: String::from("junma"),
age: 23,
};
match p {
// 这里要改为ref mut name
Person { ref mut name, age } => {
name.push_str("jinlong");
println!("name: {}, age: {}", name, age)
},
}
println!("{:?}", p);
}

最后,也可以将match value{}的value进行修饰,例如match &mut value {},这样就不需要在模式中去加ref和mut了。这对于有多个分支需要解构赋值,且每个模式中都需要ref/mut修饰变量的match非常有用。

1
2
3
4
5
6
7
8
fn main() { 
let mut x : Option<String> = Some("hello".into());
match &mut x { // 在这里borrow
Some(i) => i.push_str("world"), // 这里的i就不用再borrow
None => println!("None"),
}
println!("{:?}", x);
}

匹配守卫(match guard)

匹配守卫允许匹配分支添加额外的后置条件:当匹配了某分支的模式后,再检查该分支的守卫后置条件,如果守卫条件也通过,则成功匹配该分支。

1
2
3
4
5
6
7
let num = Some(4); 
match num {
// 匹配Some(x)后,再检查x是否小于5
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}

注意,后置条件的优先级很低。例如:

1
2
3
// 下面两个分支的写法等价
4 | 5 | 6 if y => println!("yes"),
(4 | 5 | 6) if y => println!("yes"),