Rust 模式匹配
在 match 这一页,你已经学过了基础:分支、_、用 | 把几个值合在一起,还有范围。但这只是开始。Rust 的模式(pattern)还能看进一个值的内部,取出你需要的部分,再顺便检查额外的条件——全都写在一行里。
可以把模式想象成饼干模具:你把它按在一个值上,形状对得上,Rust 就把里面的各个部分交给你;对不上,Rust 就去试下一个分支。想通这一点之后,你会发现模式无处不在:match、let、函数参数,还有 if let。
匹配守卫:再加一个 if
Section titled “匹配守卫:再加一个 if”有时光看形状还不够,还得再检查一下。匹配守卫(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,所以结果是“很舒服”。
解构元组和结构体
Section titled “解构元组和结构体”解构(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 } 的简写。
解构带数据的枚举
Section titled “解构带数据的枚举”这才是模式真正大显身手的地方。枚举的变体可以携带数据,模式在判断当前是哪个变体的同时,还能把这些数据拆出来。
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。这就是安全上的好处:编译器不会让你从错误的变体里读取数据。
嵌套模式和 @ 绑定
Section titled “嵌套模式和 @ 绑定”模式里还可以再套模式,想套多深都行。下面的 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 这样的范围只负责检查,检查完就把值扔掉了。有了 @,你就能把它留下来。
用 _ 和 .. 忽略一部分
Section titled “用 _ 和 .. 忽略一部分”很多时候你只关心其中几个部分。用 _ 跳过一个值,用 .. 跳过“剩下的全部”。
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,最后一个 = 5Mira 的等级是 7.. 在一个模式里只能出现一次。(first, .., last) 没问题,但 (.., middle, ..) 不行——Rust 没法判断你指的是哪一个值。
在 let 和函数参数里使用模式
Section titled “在 let 和函数参数里使用模式”其实每个 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);}输出:
1920x1080x: 4, y: -2[3, 7, 11]闭包 |&(a, b)| a + b 用的也是这个思路。每一项都是 &(i32, i32),模式直接把它拆成 a 和 b。在迭代器里你会经常见到这种写法。
matches! 和 let-else
Section titled “matches! 和 let-else”有两个顺手的工具,能让你不必写一整个 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("三十一"));}输出:
true3 人及格Some(31)“三十一”不是数字Nonelet-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 value6 | _ => println!("猜错了"), | ^ no value can reach this这条警告是“不可达的模式”:expected 能匹配任何值,所以没有任何值能走到 _ 这个分支。修复方法是用守卫:g if g == expected => println!("猜对了!")。
1. 用元组写 FizzBuzz
Section titled “1. 用元组写 FizzBuzz”打印 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}"), } }}输出:
FizzBuzz11Fizz1314FizzBuzz(0, 0) 必须放在最前面。如果 (0, _) 排在前面,15 就会打印成 Fizz。
2. 银行交易
Section titled “2. 银行交易”定义 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
- 模式与模式匹配——《Rust 程序设计语言》:官方章节,介绍模式能出现在哪些地方,以及模式什么时候可能匹配失败。
- 模式语法——《Rust 程序设计语言》:把各种模式写法逐一过一遍,从范围、解构到守卫和
@绑定。 - 使用 let…else 保持在“愉快路径”——《Rust 程序设计语言》:用一个完整的例子,循序渐进地讲解 let-else。
- 解构——《通过例子学 Rust》:可以运行的示例,演示如何解构元组、数组、枚举、指针和结构体。
matches!宏——标准库文档(英文):这个宏的准确语法,包括怎么加守卫。