Rust:模式匹配 - 详解


模式匹配是Rust的核心组成部分之一,甚至可以说是最大的一个语法糖。基于这个语法,程序员可以用简短的代码表达复杂的逻辑,让代码看起来更轻便。

在我自己学习这个语法的时候,网上的教程参差不齐,貌似只是把模式匹配的众多场景一一列举出来,没有形成一个逻辑体系。我收集众多资料后,希望将模式匹配以更加体系化的形式讲解。就像SQL语句一样,再复杂的语句也是由固定的一些子句组成。同样的,模式匹配也应该和SQL一样,拆成一个一个部分讲解,再把它们慢慢组合起来形成一个复杂的模式匹配。

绝大多数学习者第一次接触模式匹配这个词,应该是在学习match表达式的时候,它可以根据不同的条件,执行不同的分支。

比如:

enum Direction {
East,
West,
North,
South,
}
fn main() {
let dire = Direction::South;
match dire {
Direction::East => println!("East"),
Direction::North => println!("North"),
Direction::South => println!("South"),
Direction::West => println!("West"),
};
}

match表达式中,=>左侧是一个模式匹配,将dire这个值从上往下一个一个匹配,如果符合某个模式,就会执行=>后面的代码。

其中dire这个被匹配的值叫做待匹配表达式=>左侧的表达式叫做模式,整个=>分支又称为匹配臂

在一个模式匹配中,我将其拆解为固定的四个组成部分:单元解构修饰条件。基于这四个部分,就可以组合出一个指定的模式。随后可以把这个模式放到不同的使用场景下,比如match函数参数等等。

本博客就以单元解构修饰条件以及使用场景五大角度,把模式匹配一点点的拆解开。


单元

所谓的单元,就是指在模式匹配中可以发挥作用的最小单元。这些单元可以自己成为一个模式匹配,也可以与解构修饰条件这样的其它部件组件,形成更复杂的模式匹配。

字面量

直接写具体字面值,如果被待匹配表达式的值刚好等于这个字面量,就匹配成功

let x = 42;
match x {
42 => println!("It's the meaning of life."),
0 => println!("Zero"),
_ => println!("Other"),
}

以上代码中,将x作为待匹配表达式进行一次模式匹配,420都是字面量,它们可以直接作为一个模式。当x == 42就会触发对应的分支。

另外的,此处并非只能狭义的写入字面字面量,你也可以写入cosnt常量和static

比如:

const ZERO: i32 = 0;
const LIFE: i32 = 42;
let x = 42;
match x {
ZERO => println!("It's zero"),
LIFE => println!("The meaning of life"),
_ => println!("Something else"),
}

变量绑定

可以直接将一个变量名作为模式,如果待匹配表达式执行到对应的匹配臂,且符合条件,则将待匹配表达式中对应位置的值绑定到变量上

let x = 5;
match x {
0 => println!("I'm a zero"),
y => println!("Got y = {}", y), // y 捕获了 5
}

以上代码中,y就是一个变量绑定。x作为待匹配表达式,一开始没有匹配上字面量0,于是往下执行,发现y这条分支没有任何限制,并且let y = x这个赋值合法(结构相同),于是相当于执行let y = x后,再执行=>后面的内容。

这个语法非常重要,它是众多基础单元内最重要的一种,变量绑定的出现频率远比其它几个基础单元高。当它与后续部件结合后,会展示出惊人的表达能力。


范围 …=

有些情况,我们关心的不仅是一个字面量,而是一个范围。比如检测某个分数是否在 60–100 之间,或者某个字母是否落在 [a, z] 区间里。

Rust 提供了 范围模式匹配:使用 ..= 操作符。

语法如下:

start ..= end

表示一个闭区间,即匹配介于 [start, end] 之间的所有值。

例如:

let score = 77;
match score {
0..=59 => println!("Fail"),
60..=89 => println!("Good"),
90..=100 => println!("Excellent"),
_ => println!("Invalid score"),
}

解释:

  • 0..=59 表示 0 到 59 的所有数
  • 60..=89 匹配 60 到 89
  • 90..=100 匹配 90 到 100
  • 如果数值落入其中,即匹配成功
  1. 范围匹配只能用于支持范围比较的类型,比如 char、整型。它不能用于 f64 这些浮点数类型,因为浮点数在 Rust 中不具备完整的区间有序语义。
  2. 范围是闭区间,所以边界值也包括在内

通配符 _

当使用_作为模式,会直接忽略待匹配表达式对应位置的值

let x = 0;
match x {
42 => println!("It's the meaning of life."),
_ => println!("Other"),
}

以上模式匹配中,第一个模式匹配失败,于是匹配第二条。可以把_当做一个变量,let _ = x这个赋值合法(结构相同),因此匹配成功。但是与变量不同的是,_不会进行绑定,相当于把x这个值忽略掉了。

在与后续的其它部件结合后,可以使用这个语法直接忽略掉自己不需要的部分,也常用于match的最后一个分支,表示其它情况的通用处理路径。

而且在处理枚举的情况下,枚举值是有可能在不同版本添加新枚举的,此时_就可以处理新出现的枚举。


忽略 …

.._的功能非常类似,它也表示一种忽略,但是它在结构中,往往用于忽略多个部分

struct Point { x: i32, y: i32, z: i32 }
let p = Point { x: 1, y: 2, z: 3 };
match p {
Point { x, .. } => println!("x: {}", x),
_ => println!("Other"),
}

以上代码用了一会要讲到的语法,可以理解为p这个结构体作为待匹配表达式,第一条匹配臂只对p.x 做处理,而p.yp.z全部忽略,因此模式的写法是 Point { x, .. },表示除了x外其它的值全部忽略。

不同的是,..不能单独作为一个模式,而前三者都可以单独作为模式。


解构

刚讲完了四种基本的组成单元,它们除了自己可以作为模式,也可以按照指定的结构进行组合,这样就可以对复合类型进行精细的拆解,你可以从已经耦合在一起的众多数据中,精准拿出你需要的数据,忽略其它不重要的数据

在一个解构中,可以放入之前讲解的基础单元,后续我将使用unit来表示某个位置可以放一个基础单元


元组

当对一个元组进行模式匹配时,它的语法如下:

(unit, unit, unit, ...)

将多个单元放到小括号()内,使用逗号隔开,模式中每个位置的单元会和待匹配表达式内指定位置的值进行匹配

例如:

let triple = (1, 2, 3);
match triple {
(1, y, z) => println!("Starts with 1: ({}, {})", y, z),
_ => println!("It's a triple!"),
}
match triple {
(_, _, z) => println!("End with {}", z),
_ => println!("It's a triple!"),
}
match triple {
(.., z) => println!("End with {}", z),
_ => println!("It's a triple!"),
}

以上代码中,我展示了三个模式匹配。

在第一个模式匹配中,模式为 (1, y, z),其中1是字面量,而yz是绑定。这个模式匹配的意思是:如果第一个值是1,则匹配成功,并把yz取出来做处理。

在第二个模式匹配中,模式为 (_, _, z),其中_是通配符,z是绑定。这个模式匹配的意思是:把待匹配表达式的第三个元素取出来,绑定到z上,忽略待匹配表达式的前两个元素。

第三个模式匹配中,模式为(.., z),其中..是忽略,z是绑定。这个模式匹配和第二个模式匹配的功能是相同的。

可以看到,这种结构中可以放入任意的单元进去匹配对应位置的值,从而对一个复杂的元组做细致的拆分。

使用..可以快速提取指定个数的头尾元素:

let triple = (1, 2, 3, 4, 5);
match triple {
(a, b, ..) => println!("Starts with {} {}", a, b),
_ => println!("It's a triple!"),
}
match triple {
(.., d, e, f) => println!("End with {} {} {}", d, e, f),
_ => println!("It's a triple!"),
}
match triple {
(a, .., f) => println!("Starts with {}, End with {}", a, f),
_ => println!("It's a triple!"),
}

这次是有五个元素的元组,分别进行三次模式匹配。

第一个模式 (a, b, ..),把元组的头两个元素绑定到ab上面,其余元素忽略。

第二个模式(.., d, e, f),把元素的尾部三个元素绑定到def上,其余元素忽略。

第三个模式(a, .., f),分别把头尾的元素绑定到af上,中间元素忽略。

但是要注意的是:在每个tuple模式内部,只能使用一次..进行忽略

例如:

let triple = (1, 2, 3, 4, 5);
match triple {
(.., mid, ..) => println!("The triple has {}", mid),
_ => println!("It's a triple!"),
}
match triple {
(.., .., f) => println!("End with {}", a),
_ => println!("It's a triple!"),
}

以上两个模式匹配都是错误的,它们都使用了多次..,这会导致表意不明确,有歧义,这是Rust不允许的。

比如说 (.., mid, ..)你到底想提取第几个元素?有很多种情况符合这个模式,这就会导致歧义。

如果你想具体提取第三个元素,你可以这么做:(_, _, mid, ..),这样就可以忽略掉前两个元素。


数组

数组的解构和元组非常类似,语法如下:

[unit, unit, unit, ...]

只需要把外部的()改为[]就是数组解构。

数组的解构又分为定长数组和不定长数组两种情况。

  • 定长数组

定长数组的解构几乎完全和元组一致,既可以一个一个地匹配所有的元素,也可以使用..进行忽略

let arr = [10, 20, 30, 40];
match arr {
[first, _, third, _] => println!("first={}, third={}", first, third),
[first, .., last]    => println!("range: {} … {}", first, last),
[0..=20, second, ..] => println!("Start with 1, Second = {}", second),
_ => println!("It's a Array!"),
}

以上代码对一个数组进行解构。

第一个分支臂 [first, _, third, _] 表示取出第一个和第三个元素。

第二个分支臂 [first, .., last] 表示取出头尾元素。

第三个分支臂[0..=20, second, ..] 表示当第一个元素在[0, 20]区间内的情况下,取出第二个元素。

其实和元组一模一样。

  • 不定长数组

对于不定长数组,需要转化为切片后进行模式匹配

fn process_slice(slice: &[i32]) {
match slice {
[] => println!("Empty slice"),
[x] => println!("Single element: {}", x),
[first, second] => println!("Two elements: {}, {}", first, second),
[first, .., last] => println!("First: {}, Last: {}", first, last),
}
}

以上代码中,slice接收一个数组切片,随后对这个切片进行模式匹配。

在不定长数组的模式匹配中,既可以进行定长的匹配,也可以进行不定长的匹配。

比如前三个匹配臂 [][x][first, second]。它们分别表示当这个数组长度是012的时候,对这个分支进行匹配,并绑定对应的值到变量上。

而最后一个分支臂使用了..,那么这个模式可以匹配任意长度大于等于2的数组,因为要确保firstlast这两个变量可以绑定到值。

而这整个match是可以穷尽所有情况的,当数组长度为012分别匹配前三个分支,其余的全都能匹配上最后一个模式。


结构体

结构体的解构语法和结构体的构造语法很像,使用结构体的名称,然后在大括号中指定需要匹配的字段。

语法如下:

StructName {
field1: unit,
field2: unit,
...
}

对于每个结构体字段,都可以单独的进行模式匹配

例如:

struct Point {
x: i32,
y: i32,
}
let p = Point { x: 1, y: 2 };
match p {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x: a, y: 0 } => println!("On x-axis: {}", a),
Point { x: 0, y: b } => println!("On y-axis: {}", b),
Point { x: a, y: b } => println!("On neither axis: ({}, {})", a, b),
}

以上代码中,有一个Point结构体,有xy两个字段。

第一个模式 Point { x: 0, y: 0 } 匹配原点,两个字段都是0。

第二个模式 Point { x: a, y: 0 } 匹配X轴上的点,即Y = 0,同时将p.x字段绑定到变量a

第三个模式 Point { x: 0, y: b } 匹配Y轴上的点,即 X = 0,同时将p.y字段绑定到变量b

第四个模式 Point { x: a, y: b } 匹配任意点,并将两个字段分别绑定到ab

此处我特意将xyab区分开了,其中ab是模式匹配中的一个单元,用于变量绑定。

当模式匹配时,如果结构体的字段名与绑定的变量名相同,可以进行缩写,将x: x 缩写为 x

比如以上模式匹配等效于:

match p {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x, y: 0 } => println!("On x-axis: {}", x),
Point { x: 0, y } => println!("On y-axis: {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}

当模式匹配中,不希望处理结构体的所有字段,可以使用..来忽略其他字段

例如:

struct Point3D {
x: i32,
y: i32,
z: i32,
}
let p = Point3D { x: 1, y: 2, z: 3 };
match p {
Point3D { x: 0, y: 0, z } => println!("z is {}", z),
Point3D { x: 0, y, .. } => println!("y is {}", y),
Point3D { x, y: 0, .. } => println!("x is {}", x),
Point3D { z, .. } => println!("x and y both not zero, z is {}", z),
}

以上代码中,对结构体 Point3D进行模式匹配。

第一个模式 Point3D { x: 0, y: 0, z },当xy都是0,则输出z

第二个模式 Point { x: 0, y, .. }x = 0,输出y,同时把xy以外的所有字段忽略。

第三个模式 Point { x, y: 0, .. }y = 0,输出x,同时把xy以外的所有字段忽略。

第四个模式 Point { z, .. } 数组z,同时忽略z以外的所有字段。


枚举

枚举的解构是根据枚举变体来进行的。每个变体可能有不同的数据,因此模式匹配时需要根据变体的形式来编写模式。

无数据的枚举变体

对于无数据的枚举变体,直接使用枚举名和变体名即可

enum Direction {
East,
West,
North,
South,
}
let dire = Direction::South;
match dire {
Direction::East => println!("East"),
Direction::West => println!("West"),
_ => println!("North or South"),
}

在模式中,直接写出枚举变体的值即可,可以理解为一种字面量。同时也支持使用_进行其它值的忽略。


带数据的枚举变体

如果枚举变体带有数据,需要在模式中指定如何解构这些数据。这包括元组变体和结构体变体。

  • 元组变体

元组变体的解构类似于元组,使用小括号。

Enum::variant(unit, unit, ...)

其中 Enum::variant 表示枚举中的变体,通过()将这个变体的数据解构出来。

enum Message {
Quit,
Move(i32, i32),
Write(String),
}
let msg = Message::Move(10, 20);
match msg {
Message::Quit => println!("Quit"),
Message::Move(x, y) => println!("Move to ({}, {})", x, y),
Message::Write(s) => println!("Write: {}", s),
}

在匹配Message::Move时,用Message::Move(x, y)来解构出两个整数,并绑定到变量xy

这个过程中,可以对某个变体进行多次匹配

match msg {
Message::Quit => println!("Quit"),
Message::Move(0, y) => println!("Only move at Y {}", y),
Message::Move(x, 0) => println!("Only move at X {}", x),
Message::Move(x, y) => println!("Move to ({}, {})", x, y),
Message::Write(s) => println!("Write: {}", s),
}

以上代码对Message::Move进行了三次匹配,相比于前面一个版本,这次把x = 0y = 0的情况单独拆出来做了匹配。

  • 结构体变体

结构体变体的解构类似于结构体,使用大括号。

Enum::variant{
field1: unit,
field2: unit,
...
}

解构时,{}内部的用法和结构体解构完全相同

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
let msg = Message::Move { x: 10, y: 20 };
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(s) => println!("Write: {}", s),
}

在匹配Message::Move时,我们使用Message::Move { x, y }来解构出字段xy

与结构体相同:当字段名与变量名相同可以进行缩写,以及使用..可以对其余字段进行忽略


嵌套解构

模式匹配还支持嵌套解构,也就是说,可以在一个模式中同时解构多个层次的数据结构

总的来说,前面一共学习了三种类型的解构格式:

// 元组
(unit, unit, unit ...)
// 数组
[unit, unit, unit ...]
// 结构体
StructName {
field1: unit,
field2: unit,
...
}

事实上,更严谨一些的表述中,应该把所有unit换成patternpattern表示一个模式。

比如你可以这么做:

[(a, b), _, (c, d), ..]

以上是一个数组的解构,但是数组的第一个元素,使用了(a, b) 进行嵌套解构,因为(a, b)算作一个模式pattern

我之前说过,五个基本单元中,除了..以外,剩下的都可以单独进行模式匹配,所以_,变量绑定,..=范围,字面量都可以算作一个pattern,那么更严谨的解构格式如下:

// 元组
(pattern, pattern, pattern...)
// 数组
[pattern, pattern, pattern...]
// 结构体
StructName {
field1: pattern,
field2: pattern,
...
}

在这些pattern中,你既可以放入单元unit,也可以放入其他的解构格式,唯独对..的出现有额外限制:

  • 每层元组中允许..出现一次
  • 每层数组中允许..出现一次
  • 每层结构体中允许..出现一次,但不能单独作为field: 后面的pattern出现

注意我的措辞,我说的是“每层”,比如你可以这么干:

let arr= [(1, 2, 3), (10, 20, 30), (100, 200, 300), (10, 20, 30), (1, 2, 3)];
match arr {
[(a, b, c), second, (d, ..), .., last] => println!("do nothing"),
_ => println!("default"),
}

以上模式匹配已经有一点点复杂了,首先可以看出这是一个数组,数组中每个元素都是一个元组(i32, i32, i32)

abcd四个变量的类型都是i32,而secondlast的类型都是(i32, i32, i32)的元组,这里可以提现嵌套场景下,不同层级的变量拿到了不同类型的值。

(d, ..)中,只匹配了第一个元素,对剩下的元素进行忽略。而在外层的[]中,也用了一次..,表示忽略中间元素,从而拿到last。这两次..的使用不冲突,因为它们是独立的。


修饰

在基本的单元和各种解构场景中,其实已经能表达大多数简单逻辑。但 Rust 的模式匹配还提供了一些修饰符,它们不是独立的新模式,而是对已有模式的增强。这样的修饰符能让你更精准地控制匹配过程,或者在匹配的同时完成一些额外操作。

ref、mut

如果你尝试执行以下代码:

let mut s = String::from("hello");
match s {
x => x.push_str(" world"),
}
println!("say {}", s);

你会发现代码报错了,而且是两处错误:

  1. x.push_str 不允许,因为x不可变
  2. 编译器提示s已经被移动了,最后一行println!没有所有权

这是因为在模式匹配时,Rust 默认是按值解构的,这个时候可能会发生拷贝,或者移动(后续会在所有权章节讲解)。

对于String来说,它发生的是移动。在 x => x.push_str(" world") 时,就已经把s移动给了x,当这行代码执行完,x离开作用域,变量就会被销毁。

这里相当于执行的是let x = s;,这个绑定是一个不可变绑定,因此x.push_str()也会失败。此时就需要修饰符起作用了。

  • mut在变量绑定的前面添加一个mut,会以可变绑定的形式捕获外面的值
let mut s = String::from("hello");
match s {
mut x => x.push_str(" world"),
}

在以上代码中,x这个变量绑定的前面添加了一个mut,此时x.push_str()就合法了。

但是现在x还是把s给移动走了,内部就算修改了这个字符串,最后还是无法在外部输出,此时就需要另一个修饰符。

  • ref在变量绑定的前面添加一个ref,会以借用的形式捕获外面的值

例如:

let mut s = String::from("hello");
match s {
ref x => x.push_str(" world"),
}
println!("say {}", s);

此时最后一行println!不再报错了,因为ref x没有转移s的所有权,相当于let x = &s;

但是现在 x.push_str(" world") 还是会报错,因为这以不可变借用的形式绑定的。

实际上,ref分为两种:

  1. ref:创建不可变引用;
  2. ref mut:创建可变引用

所谓创建可变引用,其实就是把refmut两个修饰符同时用上了,但是你不能写成mut ref,前后顺序是固定的。

最终版本的代码如下:

let mut s = String::from("hello");
match s {
ref mut x => x.push_str(" world"),
}
println!("say {}", s);

现在既可以在match内部修改数据,又不会导致外部所有权丢失了。


match ergonomics

上面我们看到,匹配时如果希望用借用而不是移动,就需要手写 refref mut。不过,从 Rust 2018开始,引入了一项改进,被称为 match ergonomics(直译:匹配人机工学,意即让写法更舒适)。

它的作用是在模式匹配中,编译器能够自动推断并添加必要的 ref / ref mut,你常常不需要自己写

let x = Some(10);
match &x {
Some(v) => println!("val = {}", v), // v: &i32
None => println!("none"),
}

&x 的类型是 &Option<i32>,在模式 Some(v) 中,Rust 会自动把它理解成 Some(ref v)。所以变量 v 的类型变成了 &i32,我们直接使用即可。

在旧版 Rust 或不开启 ergonomics 时,你必须手写成:

match &x {
Some(ref v) => println!("val = {}", v),
None => println!("none"),
}

可变引用的例子:

let mut x = Some(10);
match &mut x {
Some(v) => *v += 1, // v: &mut i32
None => {}
}
println!("{:?}", x); // 输出 Some(11)

同理,这里 v 被自动推断成 &mut i32,你根本不用写 Some(ref mut v)

如果你的匹配对象本身是 &T(引用),那么编译器会帮你在模式里自动加上 ref。如果是 &mut T(可变引用),会自动加上 ref mut

因此 大多数时候你只要写 Some(x) 就够了,不必烦恼 ref 到底该放哪。 当然,手写 ref 依然是合法的,只不过大多数场景下已经不是必须。


绑定 @

所谓 @ 绑定,就是“匹配 + 绑定”的结合技。它可以在模式里既进行解构,又同时把整体值绑定到变量上

语法形式如下:

variable @ pattern
  • pattern 位置会照常进行模式匹配;
  • 如果匹配成功,会把整个被匹配的值再赋给左边的 variable

现有一个Ticket结构体:

struct Ticket {
used: bool,
owner: String,
}
impl Ticket {
fn new(s: String) -> Ticket {
Ticket{
used: false,
owner: s,
}
}
fn consume(&mut self) {
self.used = true;
println!("Ticket is used by: {}", self.owner);
}
}

这个结构体表示一张门票,带有used属性,调用consume方法消费后,used会变成true

let mut t = Ticket {
used: false,
owner: String::from("Jack"),
};
match t {
mut tic @ Ticket { used: false, .. } => tic.consume(),
_ => println!("Nothing"),
}

通过模式匹配 mut tic @ Ticket { used: false, .. } 对这张票进行消费。

这个模式匹配的意思是:对结构体t解构,要求used字段是false。如果模式匹配成功,那么把整个t赋值给tic,随后调用tic.consume()消费这张票。

@ 绑定在很多场景下很方便,比如既想检查部分条件,又想留住整体值。否则就得写两次匹配,既冗余又累赘。


条件

在表达式之外,Rust 的模式匹配还支持在匹配逻辑上增加条件限制。这些技巧让模式匹配更具表现力,避免写冗长的 if-else

多重模式 |

有时候多个模式分支的行为完全相同,这时可以用 | 把它们写在一起,表示“只要符合任一模式即可”。

语法如下:

pattern1 | pattern2 | pattern3 => {...}

假设现有以下模式匹配:

let score = 80;
match score {
0..40 => println!("focus on"),
95..=100 => println!("focus on"),
41..=94 => println!("Normal"),
_ => println!("invalid score"),
}

这个模式匹配的目的是把成绩在[0, 40][95, 100]的同学重点关注。这两个模式最后输出的结果都是一样的,但是我们很难把这两个模式统一为一个模式,因为中间发生了区间的跳跃,此时就需要多重模式了。

例如:

let score = 80;
match score {
0..=40 | 95..=100 => println!("focus on"),
41..=94 => println!("Normal"),
_ => println!("invalid score"),
}

通过|符号,把0..=4095..=100 两个模式合并为一个模式,这样就可以把它们的逻辑进行统一。


守卫 if

虽然模式匹配已经支持解构与范围,但有些逻辑判断太灵活,光靠模式本身不够描述。这个时候,我们可以给匹配分支加上一个条件判断,叫做守卫

语法形式如下:

pattern if condition => {...}

意思是:匹配 pattern 后,还要同时满足 condition 里的布尔表达式。

举个例子:

let num = 10;
match num {
n if n % 2 == 0 => println!("Even number"),
n if n % 2 == 1 => println!("Odd number"),
_ => println!("Something else"),
}

第一个分支 n if n % 2 == 0,匹配任何数并绑定到 n,同时要求 n % 2 == 0。 若守卫条件不成立,即便模式匹配成功了,也不会进入分支,而是尝试后续分支。

值得注意的是,if守卫的判断时间比模式更晚,只有待匹配表达式和模式匹配成功了,才会执行后续的守卫判断因此if守卫中可以直接使用模式中已经绑定好的变量,比如这里在if中可以直接使用n % 2来做判断。

条件守卫可以和多重模式 | 结合使用:

let ch = 'x';
match ch {
'a'..='z' | 'A'..='Z' if ch != 'x' => println!("Other letter"),
'x' => println!("The special x"),
_ => println!("Not a letter"),
}

此处判断ch是不是一个字母,但是如果ch等于'x',则需要额外处理。因此在第一行判断完它是不是一个字母后,再通过守卫判断它是不是'x'

这里要提到的注意事项是,当使用|多重模式,它整体已经构成一个模式了,你要把 'a'..='z' | 'A'..='Z' 看成一个整体,当它匹配完毕后,才会执行if守卫。


使用场景

模式匹配并不是 match 的专利,它深入渗透在 Rust 的方方面面。可以说,只要你熟悉了模式的拼装方式,就能在各种位置写出简洁又强大的代码。以下逐一介绍它们的常见使用场景。

let

也许你想不到,最常见的let就是一个模式匹配。

比如:

let x = 10;

从模式匹配的角度,x是一个模式,10就是待匹配表达式,x是一个变量绑定的单元,因此它绑定到10这个值上。

语法:

let pattern = scrutinee;

此处的scrutinee表示待匹配表达式。

let 绑定中,不必只写一个变量,你可以直接在左侧放一个模式,对值进行解构。

let (x, y, _) = (1, 2, 3);         // tuple 解构
let [a, b, ..] = [1, 2, 3];  // array 解构

(x, y) 会把元组解构成两个部分,绑定到 xy[a, b, ..] 则对数组解构,忽略掉后续元素,只保留前两个。

你甚至可以创建一个不使用的变量:

let _ = 1;

在代码编写过程中,可以有一些变量写出来之后,你没有使用它,也许是为了后续版本做准备,又或者处于其它因素。

但是这个时候编译器往往会给你报警告,表示你有一个没使用的变量。只要在变量名前面加一条下划线,就可以消除这个警告,表示你这个变量定义出来,目前就是不打算使用的

let _num = 1;

当然这和模式匹配关系就不大了,只是语法上有点类似,都使用了下划线,都有忽略的含义。


if let

你在处理Option的时候,也许经常写出以下这种代码:

let op = Option::from(10);
match op {
Some(val) => println!("Got option value: {}", val),
None => (),
}

在所有match分支中,只有一条分支是有意义的,其他分支根本不进行任何处理,这样来看match就有些冗余了。

在只有一个分支有意义、其他情况都忽略时,可以用 if let 来简化 match

语法:

if let pattern = scrutinee {
}

只有当待匹配表达式符合模式的时候,才会执行if分支内部的内容

例如:

let op = Option::from(10);
if let Some(value) = op {
println!("Value: {}", value);
}

与完整的 match 相比,if let 只写出一个你关心的模式,剩下的情况全部隐式忽略。


let else

Rust 1.65 引入的新语法,它和if let 是相反的两种语法。

if let 中,只有模式匹配成功才会执行分支,而let els 则是只有模式匹配失败才会执行分支。

语法:

let pattern = scrutinee else {
}

例如:

let op = Some(42);
let Some(x) = op else {
panic!("No value!");
};
println!("x = {}", op.unwrap());

let Some(x) = ... 尝试进行模式匹配,如果匹配失败,就直接进入 else 分支执行。

它经常用于期望值必然存在,否则立刻提前返回/报错的逻辑,让代码更优雅。


while let

在循环中,可以通过 while let 不断尝试解构,直到匹配失败为止

语法:

while let pattern = scrutinee {
}

只要待匹配表达式一直可以匹配模式,那么整个循环就会一直持续下去。

这常用于迭代地从容器里取值:

let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("Popped {}", top);
}

每次 stack.pop() 返回一个 Optionwhile let 会在 Some(top) 时执行循环体,同时自动把值绑定给 top,当返回 None 时,匹配失败,循环退出。


for

模式也可以直接写在 for 循环的迭代表达式里。

语法:

for pattern in scrutinee {
}

比如:

let map = vec![("a", 1), ("b", 2)];
for (k, v) in map {
println!("{}: {}", k, v);
}

这里 (k, v) 是一个模式,直接解构迭代器产生的元组。注意这里作待匹配表达式的不是整个数组,而是数组内的每个元素


函数参数

函数参数本身也能写成模式。

语法:

fn func_name(pattern: type, pattern: type ...) {
}

比如你可以在参数中解构元组,结构体,数组等等:

fn process_tuple((a, b, c): (i32, f64, &str)) {
println!("解构元组: a={}, b={}, c='{}'", a, b, c);
}
fn process_point(Point { x, y }: Point) {
println!("解构Point: x={}, y={}", x, y);
}
fn process_array([first, second, third]: [i32; 3]) {
println!("解构数组: [{}, {}, {}]", first, second, third);
}

再比如说,解构point的时候,你想保留它的整体:

fn process_with_binding(p @ Person { name, age, .. }: Person) {
println!("整体Person: {:?}", p);
println!("姓名: {}, 年龄: {}", name, age);
}

这里通过@保留了外层的person

再比如说,你接受了某个参数,但是打算忽略它:

fn foo(_: i32) {
}

此处就用_模式匹配来忽略一个参数。


matches! 宏

最后一个常用场景是快速布尔判断。matches! 宏接收一个表达式和一个模式,如果匹配成功,返回 true,否则 false

语法:

matches!(scrutinee, pattern);

例如:

let is_digit = matches!('5', '0'..='9');

这里 '0'..='9' 就是一个范围单元,matches! 让判定非常简洁。在部分不支持模式匹配的位置,可以通过matches!间接进行模式匹配,最后处理布尔值进行判断。


可反驳 与 不可反驳

在刚刚的众多使用场景中,它们的模式匹配也是有一定的区别的。整体分为可反驳模式不可反驳模式

  • 不可反驳的要求一定会匹配成功,不会失败
  • 可反驳模式的意思是:可能匹配成功,也可能匹配失败

结合上文所有使用场景:

使用场景模式要求
let不可反驳模式
if let可反驳模式
let else可反驳模式
while let可反驳模式
for不可反驳模式(每个迭代值必须能匹配)
函数参数不可反驳模式
matches!接受任意模式(常用可反驳模式)
match接受任意模式(常用可反驳模式)

不可反驳常用在绑定语法:let、函数参数、for 循环中的迭代变量。 因为它们必须绑定到一个值上,如果失败了,语法就失去了意义。

比如说以下代码就是不允许的:

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

此处用10去匹配了1,很明显会匹配失败,这就是一个可反驳模式,不能用于let绑定。

可反驳模式常用在 控制语法:if letwhile letlet-elsematch。因为这些语法本身就是在描述某种情况成立的时候才执行。


posted @ 2025-12-03 16:57  clnchanpin  阅读(53)  评论(0)    收藏  举报