- 教程
- 文档
【免费下载链接】book
The Rust Programming Language
match是 Rust 中最强大的控制流结构之一:它将一个值与一系列模式逐一比对,并执行首个匹配模式对应的代码分支。本篇文章基于本仓库(The Rust Programming Language 官方书稿)src/ch06-02-match.md 的完整教学内容展开,结合仓库内可编译、可运行的代码清单,系统讲解match的语法、模式绑定、Option<T>处理、穷尽性检查以及 catch-all 与_占位符的使用。读完本篇,你将能够用match写出编译器帮你保证"所有情况都已处理"的健壮枚举处理逻辑。
什么是match:值、模式与代码分支
match允许你把一个值同一系列模式(pattern)比较,然后根据哪个模式匹配来执行相应代码。模式可以由字面量、变量名、通配符等多种元素构成——关于全部模式种类的详解,参见 src/ch19-00-patterns.md(本书第 19 章)。match的强大之处来自模式的表达能力,以及编译器会确认所有可能的情况都已被处理这一事实。
可以把match表达式想象成一台硬币分拣机:硬币沿轨道滑下,轨道上排列着大小不一的孔洞,每个硬币会落入它能通过的第一个孔洞。同理,值会依次经过match中的每个模式,在第一个"恰好适合"的模式处落入对应的代码块并被执行。
第一个例子:为美国硬币计算面值(Listing 6-3)
下面是仓库中listings/ch06-enums-and-pattern-matching/listing-06-03/src/main.rs的完整代码。它定义了一个Coin枚举,并编写函数value_in_cents接收一个未知的 US 硬币,类似计数机那样判断硬币种类并返回其面值(单位:分):
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }逐行拆解这个match:
- 先写出
match关键字,后面跟一个表达式,本例中是被匹配的值coin。这看起来与if后面的条件表达式很像,但有一个重大区别:if的条件必须求值为布尔值,而match后面可以是任意类型。本例中coin的类型是文件开头定义的Coin枚举。 - 接下来是
match的各个分支(arm)。一个分支由两部分组成:一个模式 + 一段代码。第一个分支的模式是Coin::Penny,=>运算符把模式与要运行的代码分隔开,这里的代码只是值1。每个分支之间用逗号分隔。 - 当
match表达式执行时,它会按顺序把结果值与每个分支的模式比较。若某个模式匹配,就执行该模式关联的代码;若不匹配,则继续检查下一个分支——正如硬币分拣机那样。分支数量没有上限,本例中有四个分支。 - 每个分支的代码都是一个表达式,匹配分支中表达式的结果值,就是整个
match表达式返回的值。
多行分支:使用花括号
当分支代码很短(例如只返回一个值)时,通常不用花括号。但如果一个分支要运行多行代码,就必须使用花括号,此时该分支后的逗号可以省略。仓库中的listings/ch06-enums-and-pattern-matching/no-listing-08-match-arm-multiple-lines/src/main.rs演示了这一点——每次传入Coin::Penny时打印 "Lucky penny!",但仍返回代码块的最后一个值1:
fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }注意println!("Lucky penny!");语句之后没有分号结尾的1是块表达式的尾表达式,它决定了整个分支代码块的值。
模式绑定:从枚举变体中提取值
match分支的另一个实用特性是:模式可以绑定到与之匹配的值的各个部分。这正是从枚举变体中提取数据的核心手段。
沿用硬币的例子:1999 至 2008 年间,美国为 50 个州各发行了一版不同图案的 25 分硬币(quarter)。没有其他硬币带有州图案,所以只有Quarter变体带有额外数据。我们可以把Quarter变体改成包含一个UsState值,如listings/ch06-enums-and-pattern-matching/listing-06-04/src/main.rs所示:
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), }想象一下:一位朋友正在收集全部 50 个州的 25 分硬币。我们在按硬币种类分拣零钱的同时,还要喊出每个 25 分币所属的州名,以便朋友能补上缺失的那枚。在对应的match表达式中,我们为匹配Coin::Quarter变体的模式添加一个名为state的变量。当Coin::Quarter匹配时,state变量会绑定到该 25 分币所属州的值上,随后即可在分支代码中使用它,如listings/ch06-enums-and-pattern-matching/no-listing-09-variable-in-pattern/src/main.rs:
fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {state:?}!"); 25 } } }若调用value_in_cents(Coin::Quarter(UsState::Alaska)),此时coin就是Coin::Quarter(UsState::Alaska)。依次与各分支比较时,前面的分支都不匹配,直到到达Coin::Quarter(state)——此时state绑定为UsState::Alaska。随后println!表达式中使用{state:?}格式化(这依赖UsState上#[derive(Debug)]提供的调试输出能力),从而把内部的状态值从Quarter变体中取了出来。
用match处理Option<T>:经典的 plus_one 模式
上一节(本书第 6 章枚举定义部分)介绍了如何从Some情况中取出内部的T值;现在我们用match直接处理Option<T>,方式与处理Coin枚举完全相同——不再比较硬币,而是比较Option<T>的变体,match表达式的工作机制不变。
假设我们要写一个函数:接收一个Option<i32>,如果内部有值则加 1,如果没有值则原样返回None、不执行任何运算。借助match这个函数很容易写出,见listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:
fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None);逐分支推演执行过程
先看plus_one(five)的第一次执行:plus_one体内的变量x值为Some(5)。把它与第一个分支比较:
None => None,Some(5)不匹配None模式,继续下一个分支:
Some(i) => Some(i + 1),Some(5)匹配Some(i)吗?匹配!变体相同,i绑定到Some内部包含的值,于是i取得5。随后执行分支代码:给i加 1,并把总和6装进一个新的Some值返回。
再看第二次调用plus_one(None),此时x是None。进入match后先与第一个分支比较:
None => None,这次匹配成功!由于没有值可加,程序直接返回=>右侧的None。因为第一个分支已经匹配,其余分支不再比较。
"match 枚举 + 绑定内部数据 + 据此执行代码"是 Rust 代码中反复出现的惯用法:先用match匹配枚举,把变量绑定到内部数据,再基于它执行业务逻辑。初学阶段会觉得有点绕,但一旦习惯,你会在所有语言里都想要这个特性——它一直是 Rust 使用者最偏爱的能力之一。
穷尽性检查:编译器强制覆盖所有可能
match还有一个必须讨论的特性:分支的模式必须覆盖所有可能性。仓库中listings/ch06-enums-and-pattern-matching/no-listing-10-non-exhaustive-match/src/main.rs给出了一个带 bug、无法编译的plus_one版本——它漏掉了None分支:
fn plus_one(x: Option<i32>) -> Option<i32> { match x { Some(i) => Some(i + 1), } }幸运的是,这是 Rust 能帮你捕获的 bug。尝试编译这段代码,会得到listings/ch06-enums-and-pattern-matching/no-listing-10-non-exhaustive-match/output.txt中记录的错误输出:
$ cargo run Compiling enums v0.1.0 (file:///projects/enums) error[E0004]: non-exhaustive patterns: `None` not covered --> src/main.rs:3:15 | 3 | match x { | ^ pattern `None` not covered | note: `Option<i32>` defined here ... help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 4 ~ Some(i) => Some(i + 1), 5 ~ None => todo!(),可以看到,Rust 不仅知道你没有覆盖所有可能的情况,甚至明确指出了你遗漏的是哪一个模式!Rust 中的match是**穷尽(exhaustive)**的:要让代码合法,你必须穷尽每一种可能性。尤其在Option<T>的场合,Rust 阻止你忘记显式处理None情况,就等于保护你不再"在可能没有值时却假设自己一定有值"——这正是前面章节提到的"十亿美元错误"(billion-dollar mistake,即空指针假设)在 Rust 中变得不可能的原因。
Catch-all 模式与_占位符
使用枚举时,我们常常希望对少数特定值采取特殊动作,而对所有其他值执行一个默认动作。设想实现一个游戏规则:掷骰子掷出 3 时角色不移动而是获得一顶新帽子;掷出 7 时失去一顶帽子;其他点数则移动相应格数。仓库中listings/ch06-enums-and-pattern-matching/no-listing-15-binding-catchall/src/main.rs实现了这段逻辑(骰子结果硬编码为 9,其余逻辑用无函数体的占位函数表示,因为完整实现超出本例范围):
let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {}前两个分支的模式是字面量3和7;最后一个覆盖所有其他值的分支,其模式是我们命名为other的变量。other分支的代码把这个变量传给move_player函数使用。
这段代码能编译,尽管我们并没有列出u8的全部可能取值,因为最后一个模式会匹配所有未被明确列出的值。这个 catch-all 模式满足了match必须穷尽的要求。注意:catch-all 分支必须放在最后,因为模式按顺序求值——如果把它放在前面,后面的分支永远不会执行;而且如果你在 catch-all 之后又添加分支,Rust 会发出警告!
用_忽略值
当你想用 catch-all 但又不使用被匹配的值时,可以用_:_是一个特殊模式,它匹配任意值但不绑定该值。这告诉 Rust"我们不打算使用这个值",因此 Rust 不会给出未使用变量的警告。
修改游戏规则:现在只要掷出 3 或 7 以外的点数就重掷。我们不再需要用到 catch-all 捕获的值,于是把名为other的变量换成_,见listings/ch06-enums-and-pattern-matching/no-listing-16-underscore-catchall/src/main.rs:
let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {}这个例子同样满足穷尽性要求,因为我们在最后一个分支里显式忽略了所有其他值,没有任何遗漏。
用单元值表达"什么都不做"
再次修改规则:掷出 3 或 7 以外的点数时,回合内不发生任何其他事情。可以用单元值(我们在第 3 章 src/ch03-02-data-types.md 的"元组类型"一节提到的空元组)作为_分支的代码,见listings/ch06-enums-and-pattern-matching/no-listing-17-underscore-unit/src/main.rs:
let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {}这里我们在明确告诉 Rust:任何不匹配前面分支模式的值,我们都不会使用,也不希望运行任何代码。
小结与实践路径
match的完整语法总结为:match 表达式 { 模式 => 表达式, ... },其核心要点包括:
- 匹配值可以是任意类型(不同于
if只接受布尔条件); - 分支由"模式 +
=>+ 代码"组成,代码块最后表达式的值即整个match的结果; - 模式可以绑定内部数据(如
Coin::Quarter(state)、Some(i)),用于从枚举变体中提取值; match是穷尽的,漏掉任一可能性都会触发error[E0004]编译错误;- catch-all 分支必须最后出现;不需要值时应使用不绑定的
_占位符,配合单元值()表达"不执行任何动作"。
关于模式与匹配的更多内容(范围模式、|多模式、if守卫、解构等),本书 src/ch19-00-patterns.md 有系统讲解。后续章节将介绍if let语法——当match表达式略显冗长时,if let是更方便简洁的替代惯用法。你可以直接在仓库中运行这些可编译清单来动手验证:进入对应listings/ch06-enums-and-pattern-matching/下的示例目录执行cargo run,并尝试故意删掉一个分支观察编译器给出的穷尽性错误,这是理解match最直观的方式。
- 教程
- 文档
【免费下载链接】book
The Rust Programming Language
相关推荐
The Rust Programming Language:枚举与模式匹配的完美结合
The Rust Programming Language:枚举与模式匹配的完美结合 Rust编程语言中的枚举(Enum)和模式匹配(Pattern Match
教程文档Rust 枚举与模式匹配实战:Python 开发者从 Union 类型到穷尽式 match 的迁移指南
Rust 枚举与模式匹配实战:Python 开发者从 Union 类型到穷尽式 match 的迁移指南 本指南是 RustTraining 仓库《Rust fo
文档教程Rust 模式匹配完全指南:深入解析《The Rust Programming Language》第 19 章 Patterns and Matching
Rust 模式匹配完全指南:深入解析《The Rust Programming Language》第 19 章 Patterns and Matching 模式
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考