[!|center] 普若哥们儿
模式与模式匹配
模式(Patterns)是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:
- 字面值
- 解构的数组、枚举、结构体或者元组
- 变量
- 通配符
- 占位符
比如 x, (a, 3) 和 Some(Color::Red) 都是有效的模式,模式描述了数据的形状,通过某种类型的值与模式进行匹配,可以解构该类型值的具体数据,进而对数据进行操作。
模式可以出现在 Rust 程序的很多地方,事实上你已经在不经意间使用了很多模式!本章是一个所有有效模式的参考。
模式匹配的基本使用
match 分支
一个模式常用的位置是 match 表达式的分支,这个语法已经在前面的章节讲过。在形式上 match 表达式由 match 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和与值匹配时需要执行的表达式:
match 值 {
模式 => 表达式,
模式 => 表达式,
模式 => 表达式,
}
由于表达式计算后会得到一个值,因此 match 后也可以是一个表达式。
例如匹配变量 x 中 Option<i32> 值的 match 表达式:
match x {
None => None,
Some(i) => Some(i + 1),
}
这个 match 表达式中的模式为每个箭头左边的 None 和 Some(i),通过这种方式,=> 后的表达式能够获得 Option<i32> 类型的变量 x 解构后的具体数据。
match 表达式必须是 穷尽(exhaustive)的,意为 match 表达式所有可能的值都必须被考虑到,为了便于做到这一点,有一个特定的模式 _ 可以匹配所有情况,不过它从不绑定任何变量。_ 的作用有点像 C++ 语言 switch 语句中的 default 关键字。
if let 条件表达式
if let 表达式等价于match 表达式,是match 表达式 的语法糖。当只关心一个 match 分支,其余情况全部由 _ 匹配,并且不做处理时,可以将其改写为更精简 if let 语法。
if let 模式 = 值 {
/* body */
} else {
/*else */
}
等价于
match 值 {
模式 => { /* body */ },
_ => { /* else */ }, // 如果没有 else块,这相当于单元元组 ()
}
例如,下面是一个使用了 if let..else if let 的示例,该示例穷举了 Enum 类型的所有成员,还包括该枚举类型之外的情况,但即使去掉任何一个分支,也都不会报错。
enum Direction {
Up,
Down,
Left,
Right,
}
fn main() {
let dir = Direction::Down;
if let Direction::Left = dir {
println!("Left");
} else if let Direction::Right = dir {
println!("Right");
} else {
println!("Up or Down or wrong");
}
}
使用 match 分支匹配时,要求分支之间是有关联 (例如枚举类型的各个成员) 且穷尽的,但 Rust 编译器不会检查 if let 的模式之间是否有关联关系,也不检查 if let 是否穷尽所有可能情况,因此,即使在逻辑上有错误,Rust 也不会给出编译错误提醒。
while let 条件循环
一个与 if let 结构类似的是 while let 条件循环,只要 while let 的模式匹配成功,就会一直执行 while 循环内的代码。例如:
fn main() {
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 循环
在 for 循环中也存在模式匹配,模式是 for 关键字直接跟随的值,比如 for x in y 中的 x 就是一个模式,它与 y 进行匹配。例如:
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
}
这里使用 enumerate 方法适配一个迭代器来产生一个值和其在迭代器中的索引,它们位于一个元组中。第一个产生的值是元组 (0, 'a')。当这个值匹配模式 (index, value),index 将会是 0 而 value 将会是 'a',并打印出第一行输出。
let 语句
let 变量绑定时也存在模式匹配,这个你可能没发觉:
let PATTERN = EXPRESSION;
变量是一种最简单的模式,变量名位于 Pattern 位置,绑定的过程为:将表达式与模式进行匹配,并将任何模式中找到的变量名与对应值进行绑定。
例如:
let x = 5;
let (x, y) = (1, 2);
第一条语句,变量 x 就是一个模式,这个变量能够成功匹配任何值,因此在执行该语句时,将 5 绑定到变量名 x。
第二条语句,将表达式 (1,2) 和模式 (x,y) 进行匹配,匹配成功,于是为找到的变量 x 和 y 进行绑定:x=1,y=2。
如果模式中的元素数量和表达式中返回的元素数量不同,则匹配失败,编译将无法通过。
let (x,y,z) = (1,2); // 失败
函数参数
为函数参数传值和使用 let 变量绑定是类似的,本质都是在做模式匹配的操作。
fn foo(x: i32) {
// code goes here
}
形参 x 就是一个模式!类似于之前对 let 所做的,如果模式与实参匹配,将实参的值绑定到形参变量 x。
因为闭包类似于函数,闭包参数也存在模式匹配。
可反驳性
模式有两种形式:refutable(可反驳的)和 irrefutable(不可反驳的)。能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable)。一个例子就是 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败。对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable)。一个这样的例子便是 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。
函数参数、let 语句和 for 循环只能接受不可反驳的模式,因为当值不匹配时,程序无法进行有意义的操作。if let 和 while let 表达式可以接受可反驳和不可反驳的模式,但编译器会对不可反驳的模式发出警告,因为条件表达式的意义在于它能够根据成功或失败来执行不同的操作,使用不可反驳的模式没有意义。
match则支持两种模式:
- 当明确给出分支的 Pattern 时,必须是可反驳模式,这些模式允许匹配失败
- 使用
_作为最后一个分支时,是不可反驳模式,它一定会匹配成功 - 如果只有一个 Pattern 分支,则可以是不可反驳模式,也可以是可反驳模式
当模式匹配处使用了不接受的模式时,将会编译错误或给出警告。
// let变量赋值时使用可反驳的模式(允许匹配失败),编译失败
let Some(x) = some_value;
// if let处使用了不可反驳模式,没有意义(一定会匹配成功),给出警告
if let x = 5 {
// xxx
}
对于 match 来说,以下几个示例可说明它的使用方式:
match value {
Some(5) => (), // 允许匹配失败,是可反驳模式
Some(50) => (),
_ => (), // 一定会匹配成功,是不可反驳模式
}
match value {
// 当只有一个Pattern分支时,可以是不可反驳模式
x => println!("{}", x),
_ => (),
}
通常我们无需担心可反驳和不可反驳模式的区别,不过确实需要熟悉可反驳性的概念,这样当在错误信息中看到时就知道如何应对。遇到这些情况,根据代码行为的意图,需要修改模式或者使用模式的结构。
所有的模式语法
在本节中,我们收集了模式中所有有效的语法,并讨论为什么以及何时可能要使用这些语法。
匹配字面值
可以直接匹配字面值模式。如下代码给出了一些例子:
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("anything"),
}
}
匹配命名变量
命名变量是匹配任何值的不可反驳模式。
fn main() {
let x = (11, 22);
let y = 10;
match x {
(22, y) => println!("Got: (22, {})", y),
(11, y) => println!("y = {}", y), // 匹配成功,输出22
_ => println!("Default case, x = {:?}", x),
}
println!("y = {}", y); // y = 10
}
上面的 match 会匹配第二个分支,同时为找到的变量 y 进行赋值,即 y=22。这个 y 的作用域只在第二个分支对应的代码部分。第 9 行的 y 为 match 之前定义的 y=10。
多个模式
在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或(or)运算符模式。
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
上面的代码会打印 one or two。
通过 ..= 匹配值的范围
..= 语法允许你匹配一个闭区间范围内的值。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 |
但范围作为模式匹配的 Pattern 时,只允许使用全闭合的 ..= 范围语法,其他类型的范围类型都会报错。例如:
fn main() {
// 数值范围
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"),
_ => (),
}
}
匹配守卫提供的额外条件
匹配守卫(match guard)是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。这个条件可以使用模式中创建的变量。下例展示了一个 match,其中第一个分支有模式 Some(x) 还有匹配守卫 if x % 2 == 0 (当 x 是偶数的时候为真):
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {} is even", x),
Some(x) => println!("The number {} is odd", x),
None => (),
}
}
也可以在匹配守卫中使用 或 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式,比如:
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
匹配守卫与模式的优先级关系看起来像这样:
(4 | 5 | 6) if y => ...
而不是:
4 | 5 | (6 if y) => ...
@ 绑定
at 运算符(@)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式,也就是说,使用 @ 可以在一个模式中同时测试和保存变量值。下例展示了一个例子,这里我们希望测试 Message::Hello 的 id 字段是否位于 3..=7 范围内,同时也希望能将其值绑定到 id_variable 变量中以便此分支相关联的代码可以使用它。可以将 id_variable 命名为 id,与字段同名,不过出于示例的目的这里选择了不同的名称。
fn main() {
enum Message {
Hello { id: i32 },
}
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),
}
}
上例会打印出 Found an id in range: 5。通过在 3..=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。
第二个分支只在模式中指定了一个范围,分支相关代码没有一个包含 id 字段实际值的变量。id 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字段中的值,因为没有将 id 值保存进一个变量。
最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id 字段的值进行测试:任何值都会匹配此分支。
解构并分解值
模式匹配可以用于解构结构体、枚举和元组,以便使用这些值的不同部分。
解构赋值时,可使用 _ 作为某个变量的占位符,使用 .. 作为剩余所有变量的占位符。使用 .. 时不能产生歧义,例如 (..,x,..) 是有歧义的。当解构的类型包含了命名字段时,可使用 fieldname 简化 fieldname: fieldname 的书写。
解构结构体
解构 Struct 时,会将待解构的 struct 各个字段和 Pattern 中的各个字段进行匹配,并为找到的字段变量进行赋值。
当 Pattern 中的字段名和字段变量同名时,可简写。例如 P{name: name, age: age} 和 P{name, age} 是等价的 Pattern。
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;
println!("x: {}, y: {}", x, y);
// 解构时可修改字段变量名: let Point2{x: a, y: b} = p;
// 此时,变量a和b将被赋值
let ori = Point3 { x: 0, y: 0, z: 0 };
match ori {
// 使用..忽略解构后剩余的字段
Point3 { x, .. } => println!("{}", x),
}
}
也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。
下例展示了一个 match 语句将 Point 值分成了三种情况:直接位于 x 轴上(此时 y = 0 为真)、位于 y 轴上(x = 0)或不在任何轴上的点。
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})");
}
}
}
记住 match 表达式一旦找到一个匹配的模式就会停止检查其它分支,所以即使 Point { x: 0, y: 0} 在 x 轴上也在 y 轴上,这些代码也只会打印 On the x axis at 0。
解构枚举
下例 match 使用模式解构每一个内部值:
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}",)
}
}
}
解构嵌套的结构体和枚举
前面讲到的例子都只匹配了深度为一级的结构体或枚举,也可以匹配嵌套的项!例如:
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}")
}
_ => (),
}
}
解构结构体和元组
甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
ref 和 mut 修饰模式中的变量
当进行解构赋值时,很可能会将变量拥有的所有权转移出去,从而使得原始变量变得不完整或直接失效。
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字段所有权已转移
}
如果想要在解构赋值时不丢失所有权,有以下几种方式:
// 方式一:解构表达式的引用,此时name的类型为&String,age的类型为&i32
let Person{name, age} = &p;
// 方式二:解构表达式的克隆,适用于可调用clone()方法的类型
// 但Person struct没有clone()方法
// 方式三:在模式的某些字段或元素上使用ref关键字修饰变量
let Person{ref name, age} = p;
let Person{name: ref n, age} = p;
在模式中使用 ref 修饰变量名相当于对被解构的字段或元素上使用 & 进行引用。
fn main() {
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 关键字修饰模式中的变量,但这样会转移原值的所有权,此时可不要求原变量是可变的。
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 一样。
#[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);
}
注意,使用 ref 修饰变量只是借用了被解构表达式的一部分值,而不是借用整个值。如果要匹配的是一个引用,则使用 &。
fn main() {
let a = &(1, 2, 3); // a是一个引用
let (t1, t2, t3) = a; // t1,t2,t3都是引用类型&i32
let &(x, y, z) = a; // x,y,z都是i32类型
let &(ref xx, yy, zz) = a; // xx是&i32类型,yy,zz是i32类型
}
最后,也可以将 match value{} 的 value 进行修饰,例如 match &mut value {},这样就不需要在模式中去加 ref 和 mut 了。这对于有多个分支需要解构赋值,且每个模式中都需要 ref/mut 修饰变量的 match 非常有用。
fn main() {
let mut s = "hello".to_string();
match &mut s { // 对可变引用进行匹配
// 匹配成功时,变量也是对原数据的可变引用
x => x.push_str("world"),
}
println!("{}", s);
}
对引用进行解构赋值
在解构赋值时,如果解构的是一个引用,则被匹配的变量也将被赋值为对应元素的引用。
fn main() {
let t = &(1, 2, 3); // t是一个引用
let (t0, t1, t2) = t; // t0,t1,t2的类型都是&i32
let t0 = t.0; // t0的类型是i32而不是&i32,因为t.0等价于(*t).0
let t0 = &t.0; // t0的类型是&i32而不是i32,&t.0等价于&(t.0)而非(&t).0
}
因此,当使用模式匹配语法 for i in t 进行迭代时:
- 如果
t不是一个引用,则t的每一个元素都会 move 给i - 如果
t是一个引用,则i将是每一个元素的引用 - 同理,
for i in &mut t和for i in mut t也一样
对解引用进行匹配
当 match VALUE 的 VALUE 是一个解引用 *xyz 时 (因此,xyz 是一个引用),可能会发生所有权的转移,此时可使用 xyz 或 &*xyz 来代替 *xyz。
下面是一个示例:
fn main() {
// p是一个Person实例的引用
let p = &Person {
name: "junmajinlong".to_string(),
age: 23,
};
// 使用&*p或p进行匹配,而不是*p
// 使用*p将报错,因为会转移所有权
match &*p {
Person { name, age } => {
println!("{}, {}", name, age);
}
_ => (),
}
}
struct Person {
name: String,
age: u8,
}
忽略模式中的值
有时忽略模式中的一些值是有用的,比如 match 中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值:使用 _ 模式或者使用 .. 忽略所剩部分的值。
使用 _ 忽略值
我们已经使用过下划线作为匹配但不绑定任何值的通配符模式了。虽然这作为 match 表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中,如下例所示:
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
也可以在一个模式中的多处使用下划线来忽略特定值,如下例所示,这里忽略了一个五元元组中的第二和第四个值:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
}
使用 _ 前缀忽略未使用的变量
如果创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为未使用的变量可能会是个 bug。这时希望 Rust 不要警告未使用的变量,为此可以用 _ 作为变量名的开头。下例编译器会警告变量 y 未使用,不过没有警告说使用 _x。:
fn main() {
let _x = 5;
let y = 10;
}
注意,只使用 _ 和使用以下划线开头的名称不同:比如 _x 仍会将值绑定到变量,而 _ 则完全不会绑定。为了展示这个区别的意义,下例会产生一个错误。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
}
我们会得到一个错误,因为 s 绑定的值的所有权会移动进 _s,并阻止我们再次使用 s。然而只使用下划线并不会绑定值。下例能够无错编译,因为 s 没有被移动进 _:
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{:?}", s);
}
用 .. 忽略剩余值
对于有多个部分的值,可以使用 .. 语法来只使用特定部分并忽略其它值。在下例的 match 表达式中,我们只获取 x 坐标并忽略 y 和 z 字段的值:
fn main() {
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),
}
}
这里列出了 x 值,接着仅仅包含了 .. 模式。这比不得不列出 y: _ 和 z: _ 要来得简单,特别是在处理有很多字段的结构体,但只涉及一到两个字段时的情形。
下例用 first 和 last 来匹配第一个和最后一个值。.. 将匹配并忽略中间的所有值。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
然而使用 .. 必须是无歧义的,如果期望匹配和忽略的值是不明确的,比如 (.., second, ..),Rust 会报错。