模式与模式匹配
在前面的章节中,我们已经多次使用了 match 和 if let 来处理枚举类型的不同变体,这些都是 Rust 中模式匹配(pattern matching)的应用。模式匹配是 Rust 的核心特性之一,允许我们以一种简洁、强大且安全的方式检查和解构复杂数据结构。
模式(pattern) 是 Rust 中一种特殊语法,用来与某个值的结构进行匹配,无论类型简单还是复杂。模式由以下部分的某种组合构成: 字面值、解构后的数组/枚举/结构体/元组、变量、通配符、占位符。
Rust 的模式匹配具有以下核心特性:
- 穷尽性:
match要求覆盖被匹配值的所有可能情况,否则编译报错,保证运行时安全 - 表达式性质:
match是一个表达式,可以有返回值,支持直接赋值给变量 - 按序匹配: 从第一个分支开始逐一检查,执行第一个匹配成功的分支,之后不再继续
- 所有权规则: 匹配默认转移被匹配值的所有权,可用
&避免转移
模式可以出现在六种位置,不同位置对可反驳性的要求不同:
| 位置 | 形式 | 接受的模式类型 |
|---|---|---|
match 分支 | match x { PATTERN => expr } | 可反驳(末尾 _ 不可反驳) |
let 语句 | let PATTERN = expr | 仅不可反驳 |
if let 表达式 | if let PATTERN = expr { } | 可反驳和不可反驳 |
while let 循环 | while let PATTERN = expr { } | 可反驳和不可反驳 |
for 循环 | for PATTERN in iter { } | 仅不可反驳 |
| 函数参数 | fn f(PATTERN: Type) | 仅不可反驳 |
模式可以出现的位置
match 分支
match 将值与多个分支的模式逐一比较,执行第一个匹配成功的分支。基本语法如下:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
_ => EXPRESSION, // 通配符,匹配所有剩余情况
}match 会从上到下依次检查分支,一旦匹配成功即停止。match 要求穷尽所有可能情况(exhaustive),可以用 other(绑定值)或 _(丢弃值)作为最后一个分支来覆盖剩余情况:
#[derive(Debug)]
enum Direction {
Up,
Down,
Left,
Right,
}
let dir = Direction::Down;
match dir {
Direction::Up => println!("向上"),
Direction::Down => println!("向下"),
other => println!("其他方向: {:?}", other), // 绑定值,可使用
// _ => println!("其他"), // 丢弃值,不使用内容
}match 作为表达式: match 可以返回值并赋值给变量,所有分支必须返回相同类型:
let x = (11, 22);
// 赋值给变量: 所有分支必须返回相同类型(此处为 i32)
let y = match x {
(a, 11) => {
println!("{}", a);
a // 返回 i32
},
(a, b) => {
println!("{}, {}", a, b);
a + b // 返回 i32
},
};
// 作为独立表达式(不赋值给变量): 分支返回 () 即可
match x {
(a, b) => println!("{}, {}", a, b), // 隐式返回 ()
}let 语句
变量绑定本质上也是一种模式匹配!
let PATTERN = EXPRESSION 将右侧表达式与左侧模式进行匹配,并将找到的变量名逐一赋值:
// 最简单的模式: 变量名
let x = 5; // 将 5 匹配到模式 x
// 元组解构: 将 (1, 2, 3) 与模式 (x, y, z) 匹配
let (x, y, z) = (1, 2, 3); // ✅ x=1, y=2, z=3
// ❌ 模式元素数量不匹配
let (x, y) = (1, 2, 3); if let / let-else
if let 是只关心一种匹配情况时 match 的简写形式。如果匹配成功就执行代码块,否则跳过(或执行 else 块):
// 只关心 Some 变体
if let Some(x) = some_value {
println!("有值: {}", x);
} else {
println!("没有值");
}if let 可以与 else if、else if let、else 自由组合,形成多条件分支:
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("使用最爱的颜色: {}", color);
} else if is_tuesday {
println!("周二用绿色!");
} else if let Ok(age) = age {
if age > 30 {
println!("用紫色");
} else {
println!("用橙色");
}
} else {
println!("用蓝色");
}注意: 与
match不同,编译器不检查if let分支之间是否穷尽或有逻辑关联,即使遗漏某种情况也不会编译报错。
if let 链式条件: 用 && 将多个 let 解包和布尔条件串联在同一个 if let 中:
// 同时解包多个 Option
if let Some(a) = opt1 && let Some(b) = opt2 {
println!("{a}, {b}"); // a、b 均已绑定
}
// 解包并附加条件
if let Some(x) = opt && x > 0 {
println!("{x}");
}while let 同样支持 && 链式条件:
while let Some(x) = iter.next() && x > 0 {
println!("{x}");
}let-else(守卫解构): 如果模式不匹配,则必须在 else 块中用 return、break、panic! 等中止当前流程;如果匹配成功,绑定的变量直接作用于当前作用域(而非 if 块内):
let San::jump(distance) = san else {
panic!("不是跳跃状态");
};
println!("跳了 {} 米", distance); // distance 在当前作用域中直接可用
// 在函数中配合 return 使用
fn check_jump(san: San) -> i32 {
let San::jump(d) = san else {
return 0; // 不是跳跃状态,返回 0
};
d
}
if let表达"如果匹配则做某事",let-else表达"必须匹配,否则退出",语义更明确。else块中必须改变控制流,不能继续执行后续代码。
while let 条件循环
只要模式持续匹配,while let 就一直执行循环体:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// stack.pop() 有元素时返回 Some,无元素时返回 None
while let Some(top) = stack.pop() {
println!("{}", top); // 依次输出 3, 2, 1
}
// pop() 返回 None 时,匹配失败,循环退出for 循环
for 循环中 in 之前的控制变量就是一个模式,可以直接用来解构:
let v = vec!['a', 'b', 'c'];
// (index, value) 是模式,解构 enumerate() 返回的元组
for (index, value) in v.iter().enumerate() {
println!("第 {} 个元素是 {}", index, value);
}函数参数
函数参数也是模式,可以直接在参数位置进行解构(闭包参数同理):
// 参数 &(x, y) 是模式,直接解构引用元组
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("当前坐标: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point); // x=3, y=5
}
// 闭包参数同样支持模式解构
let print_point = |&(x, y): &(i32, i32)| {
println!("({}, {})", x, y);
};
print_point(&point);可反驳性
| 类型 | 说明 |
|---|---|
| 不可反驳模式(irrefutable) | 对任意可能值都能匹配成功,必须匹配,否则编译报错 |
| 可反驳模式(refutable) | 对某些值可能匹配失败,失败时不执行对应代码 |
let 变量赋值、for 循环、函数传参只接受不可反驳模式;if let、while let、let-else 接受可反驳模式;match 两种都支持: 明确指定的分支是可反驳的,末尾的 _ 或变量名是不可反驳的。
// ❌ let 中使用可反驳模式: 若为 None 则无法匹配,编译失败
let Some(x) = some_option_value; // ✅ 改用 let-else 处理可反驳模式
let Some(x) = some_option_value else {
return;
};// ⚠️ if let 中使用不可反驳模式: 一定会成功,没有意义,产生警告
if let x = 5 {
println!("{x}");
}完整模式语法
匹配字面值
直接用字面量作为模式:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}匹配命名变量
模式中的变量名会遮蔽(shadow)外部同名变量,成为一个全新的局部绑定:
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("匹配到: y = {y}"), // 此 y 是新变量,值为 5,遮蔽外部 y
_ => println!("默认: x = {x:?}"),
}
println!("x = {x:?}, y = {y}");
// 输出:
// 匹配到: y = 5
// x = Some(5), y = 10分支内绑定的变量只在该分支作用域内有效,分支结束后外部变量恢复。
多选一模式 |
使用 | 匹配多个模式,表示"或":
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}范围匹配 ..= / ..
匹配连续范围,支持闭合范围 ..= 和半开范围 ..,适用于数字和字符类型。推荐使用 ..=:
// 数值范围
let x = 79;
match x {
0..=59 => println!("不及格"),
60..=89 => println!("良好"),
90..=100 => println!("优秀"),
_ => println!("超出范围"),
}
// 字符范围
let c = 'c';
match c {
'a'..='j' => println!("前段字母"),
'k'..='z' => println!("后段字母"),
_ => (),
}当两个范围首尾相邻但是却不连续时(如
60..89紧接89..=100,不包含 89),编译器会认为你可能写错了,发出non_contiguous_range_endpoints警告。
解构赋值
模式匹配可以将复合类型拆解为独立变量,称为解构(destructuring)。
结构体
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 0, y: 7 };
// 字段名与变量名相同时可简写(等价于 Point { x: x, y: y })
let Point { x, y } = p;
// 重命名: 将 x 绑定到 a,y 绑定到 b
let Point { x: a, y: b } = p;
// 在 match 中解构,支持字面值约束
match p {
Point { x, y: 0 } => println!("在 x 轴上: x = {x}"),
Point { x: 0, y } => println!("在 y 轴上: y = {y}"),
Point { x, y } => println!("不在轴上: ({x}, {y})"),
}枚举
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移动到 ({x}, {y})"),
Message::Write(text) => println!("文本: {text}"),
Message::ChangeColor(r, g, b) => println!("颜色: ({r}, {g}, {b})"),
}
// 输出: 颜色: (0, 160, 255)嵌套的结构体和枚举
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
ChangeColor(Color),
// ...其他变体
}
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => println!("RGB: {r}, {g}, {b}"),
Message::ChangeColor(Color::Hsv(h, s, v)) => println!("HSV: {h}, {s}, {v}"),
_ => (),
}元组与混合解构
// 解构元组
let (a, b, c) = (1, 2, 3);
// 混合解构: 同时解构元组和结构体
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });忽略模式中的值
用 _ 忽略整个值
_ 匹配任何值但不绑定,编译器会忽略它,可用于函数参数或 match 的通配分支:
fn foo(_: i32, y: i32) {
println!("只用 y: {y}");
}用嵌套的 _ 忽略部分值
// 忽略 Some 内部的值
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => println!("已有值,不覆盖"),
_ => { setting_value = new_setting_value; }
}
// 忽略元组中的部分元素
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => println!("{first}, {third}, {fifth}"),
}用 _x 忽略未使用变量
变量名以 _ 开头可消除未使用警告,但注意 _x 仍会绑定值(会转移所有权),而单独的 _ 不绑定:
fn main() {
let _x = 5; // 不产生未使用警告
let y = 10; // ⚠️ 会产生未使用变量警告
}let s = Some(String::from("Hello!"));
if let Some(_s) = s { // _s 获取了 s 的所有权,后续 s 不可用
if let Some(_) = s { // _ 不绑定,s 所有权不转移
println!("found a string");
}
println!("{s:?}");用 .. 忽略值的剩余部分
.. 自动扩展为所需数量的忽略项,但使用时不能产生歧义:
struct Point { x: i32, y: i32, z: i32 }
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x = {x}"), // 忽略 y 和 z
}
// 元组: 只取首尾
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => println!("头 {first},尾 {last}"),
}
// ❌ 有歧义: Rust 无法确定 second 前后各忽略多少个
match numbers {
(.., second, ..) => println!("{second}"),
}匹配守卫(match guard)
match 分支模式后面可以加 if 条件作为额外约束,模式和守卫都满足才会选择该分支:
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("{x} 是偶数"),
Some(x) => println!("{x} 是奇数"),
None => (),
}守卫可以引用外部变量,避免变量遮蔽问题:
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("n = {n}"), // 与外部 y 比较,不遮蔽 y
_ => println!("默认: x = {x:?}"),
}| 与守卫结合时,守卫作用于整个 | 组合(等价于给组合加括号):
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"), // 等价于 (4 | 5 | 6) if y
_ => println!("no"),
}
// 输出: no匹配守卫语法(
pattern if condition)仅在match中可用;在if let/while let中,可通过 Rust 2024 的&&链式条件实现类似效果,例如if let Some(x) = opt && x > 0 { ... }。
@ 绑定
@ 允许在测试值是否匹配某个模式的同时,将该值绑定到一个变量:
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => println!("id 在 3~7 范围内: {id}"),
Message::Hello { id: 10..=12 } => println!("id 在 10~12 范围内"),
Message::Hello { id } => println!("其他 id: {id}"),
}简单示例:
let x = 5;
match x {
n @ 1..=10 => println!("1 到 10: {n}"),
n @ 11..=20 => println!("11 到 20: {n}"),
_ => println!("不在范围内"),
}解构赋值与所有权
解构赋值时,非 Copy 类型的字段会发生所有权转移,导致原变量部分失效:
struct Person {
name: String,
age: i32,
}
let p = Person { name: String::from("zhangsan"), age: 23 };
let Person { name, age } = p; // name 从 p 中 Move 出来
println!("{}", name); // ✅
println!("{}", age); // ✅
// println!("{}", p.name); // ❌ name 字段所有权已转移用 ref 借用而非转移
在模式的变量名前加 ref,表示借用该字段而非转移所有权(等价于对该字段取 &):
let p = Person { name: String::from("zhangsan"), age: 23 };
// name 借用 p.name,所有权不转移
let Person { ref name, age } = p;
println!("{}", name); // ✅
println!("{}", p.name); // ✅ p.name 仍然有效| 写法 | name 的类型 | 原字段是否仍可用 |
|---|---|---|
let Person { name, .. } = p | String(Move) | ❌ |
let Person { ref name, .. } = p | &String(借用) | ✅ |
let ref x = value等价于let x = &value,均得到引用类型。
用 ref mut 可变借用
需要修改字段但不转移所有权时,使用 ref mut:
let mut p = Person { name: String::from("zhangsan"), age: 23 };
match p {
Person { ref mut name, age } => {
name.push_str("_san"); // 可修改
println!("name: {}, age: {}", name, age);
}
}
println!("{:?}", p); // ✅ p 仍然有效也可以通过
match &mut p { ... }对整个match的目标取可变引用,这样所有分支中无需手动加ref mut,对有多个分支的match尤为方便。
对引用进行解构
解构一个引用时,绑定的变量也是对应元素的引用:
let t = &(1, 2, 3); // t 是引用
let (t0, t1, t2) = t; // t0, t1, t2 的类型都是 &i32
let t0 = t.0; // t0 是 i32(.0 自动解引用)
let t0 = &t.0; // t0 是 &i32(对字段取引用)
// 用 & 在模式中匹配引用: 解构后得到值类型
let &(x, y, z) = t; // x, y, z 都是 i32在 for 循环迭代时同样适用:
let v = vec![1, 2, 3];
for i in &v { /* i 是 &i32,v 仍可用 */ }
for i in v { /* i 是 i32,v 的所有权被消耗 */ }