跳转到内容

Rust 模式匹配

在 match 这一页,你已经学过了基础:分支、_、用 | 把几个值合在一起,还有范围。但这只是开始。Rust 的模式(pattern)还能看进一个值的内部,取出你需要的部分,再顺便检查额外的条件——全都写在一行里。

可以把模式想象成饼干模具:你把它按在一个值上,形状对得上,Rust 就把里面的各个部分交给你;对不上,Rust 就去试下一个分支。想通这一点之后,你会发现模式无处不在:match、let、函数参数,还有 if let。

有时光看形状还不够,还得再检查一下。匹配守卫(match guard)就是写在模式后面的 if 条件。只有模式对得上,并且条件为真,这个分支才会执行。

fn main() {
let temperatures = [-5, 0, 18, 35];
for t in temperatures {
let feeling = match t {
t if t < 0 => "冰冷刺骨",
0 => "正好零度",
t if t < 25 => "很舒服",
_ => "很热",
};
println!("{t}°C:{feeling}");
}
}

输出:

-5°C:冰冷刺骨
0°C:正好零度
18°C:很舒服
35°C:很热
▶ 动手试一试

Rust 会从上到下依次检查各个分支,所以顺序很重要。18 没通过第一个守卫,也不是 0,但满足 t < 25,所以结果是“很舒服”。

解构(destructuring)就是把一个值拆成几个部分。对于元组,按元组的形状来写模式;对于结构体,写出你想要的字段名。

struct Point {
x: i32,
y: i32,
}
fn main() {
let pair = (3, -7);
match pair {
(0, 0) => println!("原点"),
(x, 0) => println!("在 x 轴上,位置是 {x}"),
(0, y) => println!("在 y 轴上,位置是 {y}"),
(x, y) => println!("在别的地方:({x}, {y})"),
}
let p = Point { x: 0, y: 5 };
match p {
Point { x: 0, y } => println!("在 y 轴上,y = {y}"),
Point { x, y: 0 } => println!("在 x 轴上,x = {x}"),
Point { x, y } => println!("在 ({x}, {y})"),
}
}

输出:

在别的地方:(3, -7)
在 y 轴上,y = 5
▶ 动手试一试

注意这里两种写法混在一起用。0 是固定值,必须完全相等才算匹配。x 和 y 是新变量,那个位置上是什么值,它们就接住什么值。Point { x, y } 是 Point { x: x, y: y } 的简写。

这才是模式真正大显身手的地方。枚举的变体可以携带数据,模式在判断当前是哪个变体的同时,还能把这些数据拆出来。

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Square(f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Square(side) => side * side,
}
}
fn main() {
let shapes = [
Shape::Circle { radius: 2.0 },
Shape::Rectangle { width: 3.0, height: 4.5 },
Shape::Square(1.5),
];
for shape in &shapes {
println!("面积 = {:.2}", area(shape));
}
}

输出:

面积 = 12.57
面积 = 13.50
面积 = 2.25
▶ 动手试一试

不先证明这个值是 Circle,你就拿不到 radius。这就是安全上的好处:编译器不会让你从错误的变体里读取数据。

模式里还可以再套模式,想套多深都行。下面的 Command 枚举里装着一个 Direction 枚举,一个分支就能同时匹配两层。

@ 绑定让你既能用模式检查一个值,又能把它保存到变量里。n @ 1..=9 的意思是“检查它是否在 1 到 9 之间,并把它叫作 n”。

enum Direction {
Up,
Down,
}
enum Command {
Move(Direction, u32),
Say(String),
Quit,
}
fn run(cmd: Command) {
match cmd {
Command::Move(Direction::Up, steps @ 1..=9) => println!("向上 {steps}(小步)"),
Command::Move(Direction::Up, steps) => println!("向上 {steps}(大跳!)"),
Command::Move(Direction::Down, steps) => println!("向下 {steps}"),
Command::Say(text) => println!("说:{text}"),
Command::Quit => println!("再见"),
}
}
fn main() {
run(Command::Move(Direction::Up, 3));
run(Command::Move(Direction::Up, 50));
run(Command::Move(Direction::Down, 2));
run(Command::Say(String::from("你好")));
run(Command::Quit);
}

输出:

向上 3(小步)
向上 50(大跳!)
向下 2
说:你好
再见
▶ 动手试一试

没有 @ 的时候,像 1..=9 这样的范围只负责检查,检查完就把值扔掉了。有了 @,你就能把它留下来。

很多时候你只关心其中几个部分。用 _ 跳过一个值,用 .. 跳过“剩下的全部”。

struct Player {
name: String,
level: u32,
gold: u32,
}
fn main() {
let rgb = (255, 128, 0);
let (red, _, _) = rgb;
println!("红色 = {red}");
let numbers = (1, 2, 3, 4, 5);
let (first, .., last) = numbers;
println!("第一个 = {first},最后一个 = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 };
let Player { name, level, .. } = hero;
println!("{name} 的等级是 {level}");
}

输出:

红色 = 255
第一个 = 1,最后一个 = 5
Mira 的等级是 7
▶ 动手试一试

.. 在一个模式里只能出现一次。(first, .., last) 没问题,但 (.., middle, ..) 不行——Rust 没法判断你指的是哪一个值。

其实每个 let 都是一个模式,每个函数参数也是。所以值一到手,你就可以当场解构。

fn print_coords(&(x, y): &(i32, i32)) {
println!("x: {x}, y: {y}");
}
fn main() {
let (width, height) = (1920, 1080);
println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)];
let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect();
println!("{:?}", sums);
}

输出:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ 动手试一试

闭包 |&(a, b)| a + b 用的也是这个思路。每一项都是 &(i32, i32),模式直接把它拆成 a 和 b。在迭代器里你会经常见到这种写法。

有两个顺手的工具,能让你不必写一整个 match。

matches!(value, pattern) 返回 true 或 false。只需要一个“是/否”的答案时,比如在 if 或 .filter() 里,用它正合适。这里同样可以加守卫。

let-else 用来处理“把它拆开,不行就马上离开”的情况。写法是 let PATTERN = value else { ... };。如果模式对得上,这些变量在函数剩下的部分里都能用;如果对不上,就执行 else 块,而这个块必须离开——用 return、break、continue,或者 panic。

fn parse_age(input: &str) -> Option<u32> {
let Ok(age) = input.parse::<u32>() else {
println!("“{input}”不是数字");
return None;
};
Some(age)
}
fn main() {
let grade = 'B';
println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66];
let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count();
println!("{passed} 人及格");
println!("{:?}", parse_age("31"));
println!("{:?}", parse_age("三十一"));
}

输出:

true
3 人及格
Some(31)
“三十一”不是数字
None
▶ 动手试一试

let-else 能让“顺利路径”(happy path)保持扁平。不用把代码一层层嵌进 if let ... { } 里,而是先把坏情况处理掉,然后接着往下写。它和 Option、Result 搭配起来特别好用。

守卫不算进“覆盖了所有情况”。 编译器没法推理你的 if 条件,所以即使这两个守卫其实已经覆盖了所有整数,下面的代码还是编译不过:

fn main() {
let n: i32 = 7;
match n {
x if x < 0 => println!("负数"),
x if x >= 0 => println!("零或正数"),
}
}
error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered
--> src/main.rs:3:11
|
3 | match n {
| ^ pattern `i32::MIN..=i32::MAX` not covered
|
= note: the matched value is of type `i32`
= note: match arms with guards don't count towards exhaustivity

这个错误的意思是:模式没有穷尽,i32::MIN..=i32::MAX(也就是 i32 的所有值)没有被覆盖。最后一条 note 说得很清楚:带守卫的分支不计入穷尽性检查。修复方法:把最后一个分支改成不带守卫的兜底分支,例如 _ => println!("零或正数")。

模式里的变量名是一个新变量,而不是比较。 下面这段代码看起来像是在检查 guess == expected,其实不是:

fn main() {
let expected = 3;
let guess = 5;
match guess {
expected => println!("猜对了!"),
_ => println!("猜错了"),
}
}

它能通过编译,而且会打印 猜对了!,因为分支里的 expected 只是新建了一个变量,任何值都会被它接住。Rust 会警告你:

warning: unreachable pattern
--> src/main.rs:6:9
|
5 | expected => println!("猜对了!"),
| -------- matches any value
6 | _ => println!("猜错了"),
| ^ no value can reach this

这条警告是“不可达的模式”:expected 能匹配任何值,所以没有任何值能走到 _ 这个分支。修复方法是用守卫:g if g == expected => println!("猜对了!")。

打印 9 到 15,但遇到 3 的倍数打印 Fizz,5 的倍数打印 Buzz,同时是两者倍数的打印 FizzBuzz。要求只用一个对元组的 match,不要写一串 if。提示:对 (n % 3, n % 5) 进行匹配。余数为 0 表示“能整除”,_ 表示“不关心”。

查看答案
fn main() {
for n in 9..=15 {
match (n % 3, n % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
_ => println!("{n}"),
}
}
}

输出:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ 动手试一试

(0, 0) 必须放在最前面。如果 (0, _) 排在前面,15 就会打印成 Fizz。

定义 enum Transaction { Deposit(u32), Withdraw(u32), Fee }。balance 从 100 开始,然后按顺序处理这些交易:存入 50、取出 30、取出 500、扣一次手续费。手续费是 5。如果取款金额比余额还大,就拒绝这笔取款,并打印一条消息。最后打印余额。提示:用匹配守卫 Transaction::Withdraw(amount) if amount <= balance,后面再加一个处理拒绝情况的 Withdraw 分支。

查看答案
enum Transaction {
Deposit(u32),
Withdraw(u32),
Fee,
}
fn main() {
let mut balance: u32 = 100;
let history = [
Transaction::Deposit(50),
Transaction::Withdraw(30),
Transaction::Withdraw(500),
Transaction::Fee,
];
for t in history {
match t {
Transaction::Deposit(amount) => balance += amount,
Transaction::Withdraw(amount) if amount <= balance => balance -= amount,
Transaction::Withdraw(amount) => println!("已拒绝:无法取出 {amount}"),
Transaction::Fee => balance -= 5,
}
}
println!("余额 = {balance}");
}

输出:

已拒绝:无法取出 500
余额 = 115
▶ 动手试一试

第二个 Withdraw 分支没有守卫,所以被第一个分支拒之门外的取款都会落到它这里。少了它,这个 match 就编译不过:Rust 检查是否覆盖了所有情况时,不会把守卫算进去。

  • 匹配守卫(pattern if condition)给分支加上一个额外的检查。
  • 模式可以解构元组、结构体和枚举变体,而且可以层层嵌套。
  • name @ pattern 在检查值的同时,把它保存到变量里。
  • _ 跳过一个值,.. 跳过剩下的全部。模式也能用在 let、函数参数和闭包里。
  • matches! 快速给出 true/false;let-else 要么拆开一个值,要么提前离开。

小测验

1. 在 match 分支里,n @ 1..=9 有什么作用?

2. 在 let-else 里,else 块必须做什么?

3. let (first, .., last) = (1, 2, 3, 4, 5); 会得到什么?

4. 为什么只有带守卫分支的 match 经常编译不过?

得分: 0 / 4