用类型表达:没有 null,也没有异常
模式匹配:match 不只是 switch
它在解决什么
上一篇说 enum 能把「有哪几种情况」写进类型。 但把情况写进类型只是一半 —— 另一半是每次处理它的时候,编译器都逼你 把每种情况都考虑到。
这一半由 match 承担。它和 switch 长得像,实际差三件事:
- 它是表达式,有值,可以直接
let x = match ...。 - 它穷尽,漏一种情况编译不过。
- 它的分支不是常量比较,是模式 —— 能解构、能带条件、能绑定。
穷尽性:enum 的价值在这里兑现
漏掉一个变体就是 E0004,而且编译器会把漏掉的那个
变体的名字告诉你。
这条规则真正的价值不在写的时候,在改的时候:给 enum 加一个变体,
所有没考虑它的 match 会当场全部报错,编译器替你列出了待办清单。
在用整数常量 + switch 的语言里,这件事只能靠搜索和记性。
🚨 但一个 _ 就能把它作废 —— 而且是安静地
这两段代码都编译通过,差别只在输出:
match s {
Status::Draft => "草稿",
_ => "其他", // ← 把 Published 和 Archived 都吞了
}
想象这个 _ 是半年前写的,而 Archived 是上周加的。
编译器一句话都不会说 —— 新变体安静地落进了「其他」,
而上面那条「加变体会让所有地方报错」的保证,在这个文件里已经不存在了。
⇒ 判据:_ 只用在「未来新增的都该走这条」确实成立的地方。
典型的成立场景是匹配外部数据(HTTP 状态码、用户输入的字符串)—— 那些东西本来就是开放集合。而匹配自己定义的 enum 时, 多写两行远比丢掉穷尽性划算。
📌 有个折中写法值得知道:把兜底分支写成 other => ... 而不是 _ => ...
并不能救回穷尽性,但至少下次读代码的人能看到那个值被用到了哪里。
真正想保留穷尽性又不想写全,只能老实列。
解构可以一层层嵌下去
模式不是「一个值」,是「一个形状」。enum 变体里的 struct 字段里的字面量, 一次匹配完:
Shape::Circle { center: Point { x: 0, y: 0 }, r } => format!("原点圆,半径 {r}"),
这一行同时做了四件事:判断是不是 Circle、判断 center 的 x 和 y 是不是 0、
把 r 绑出来。在别的语言里这是四个 if 加两次取字段。
.. 表示「剩下的字段不关心」:Point { x: x1, .. } 只要 x。
分支顺序有意义
match 从上往下,第一个匹配上的赢。
把更具体的分支挪到宽的后面, 它就永远轮不上了 —— 而这件事只是一条警告,不是错误: 对照项照常编译通过,只是输出变了。
⇒ 写法上的习惯:从最具体排到最宽泛。发现某个分支好像没生效时, 先往上看有没有更宽的分支把它截了。
守卫:模式管形状,if 管取值
模式里表达不了的条件(比较、取模、调方法)放进守卫:
x if x < 0 => "负数",
⚠️ 守卫不参与穷尽性检查。 编译器不会去证明
x < 0 和 x % 2 == 0 合起来覆盖了所有整数 —— 它做不到,
所以带守卫的 match 通常还是需要一个兜底分支。
这不是缺陷,是能力边界:一旦条件里可以写任意代码,穷尽性就变成了 不可判定的问题。知道这条边界在哪,比记住语法有用。
@:既要判断范围,又要拿到值
范围模式 1..=9 能判断,但判断完那个值就没名字了。
@ 让你一边判断一边起名:
small @ 1..=9 => format!("个位数 {small}"),
if let 和 let ... else 各自的位置
match 写全所有分支有时是杀鸡用牛刀。两个简写各有各的场景:
if let —— 只关心一种情况,而且后续逻辑就在那个花括号里:
if let Some(name) = config.name {
println!("{name}");
}
let ... else —— 只关心一种情况,但后续逻辑在花括号外面:
let Ok(n) = s.parse::<u16>() else {
return 8080;
};
n // ← n 在这里仍然可用
⭐ 两者的区别就是这一点:if let 把绑出来的名字困在块里,
于是「解析失败就返回默认值」这种写法会被迫嵌套下去;
let ... else 把失败分支提前送走,主线保持平铺。
⚠️ 所以 else 块必须发散 —— return、break、continue、panic! 都行,
但不能返回一个值。对照项返回 8080 就编译不过。
道理很直白:如果 else 能正常走完,后面那个 n 就没有值可用了。
⇒ 一条实用的判据:写 if let 时如果发现自己在往右缩进,
多半应该换成 let ... else。
下一步
到这里,enum 和 match 这套组合已经完整了。接下来看标准库怎么用它 ——
见《Option:没有 null 怎么活》:
它就是一个只有两个变体的普通 enum,而它替掉了 null 这个
「价值十亿美元的错误」。
全部篇目见 Rust 教程首页。
本篇示例
下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。
穷尽性:漏一个变体就编译不过
enum Status {
Draft,
Published,
Archived,
}
fn label(s: &Status) -> &'static str {
match s {
Status::Draft => "草稿",
Status::Published => "已发布",
Status::Archived => "已归档",
}
}
fn main() {
println!(
"{} {} {}",
label(&Status::Draft),
label(&Status::Published),
label(&Status::Archived)
);
}编译通过 · 输出 "草稿 已发布 已归档\n"
对照:删掉 `Archived` 那个分支
enum Status {
Draft,
Published,
Archived,
}
fn label(s: &Status) -> &'static str {
match s {
Status::Draft => "草稿",
Status::Published => "已发布",
}
}
fn main() {
println!(
"{} {} {}",
label(&Status::Draft),
label(&Status::Published),
label(&Status::Archived)
);
}编译不过:error[E0004]
对照项报 `E0004: non-exhaustive patterns`,而且编译器会**把漏掉的那个变体名字告诉你**。这是 enum 相对「一堆常量」的核心优势:加一个变体,所有没考虑它的地方当场全部报错。
🚨 一个 `_` 就能把穷尽性作废,而且是**安静地**
enum Status {
Draft,
Published,
Archived,
}
fn label(s: &Status) -> &'static str {
match s {
Status::Draft => "草稿",
_ => "其他",
}
}
fn main() {
println!("{} {}", label(&Status::Published), label(&Status::Archived));
}编译通过 · 输出 "其他 其他\n"
对照:把 `_` 换成逐个写全的分支
enum Status {
Draft,
Published,
Archived,
}
fn label(s: &Status) -> &'static str {
match s {
Status::Draft => "草稿",
Status::Published => "已发布",
Status::Archived => "已归档",
}
}
fn main() {
println!("{} {}", label(&Status::Published), label(&Status::Archived));
}编译通过,输出:"已发布 已归档\n"
⭐ 两边**都编译通过**,差别只在输出:带 `_` 的版本把两个本该各有说法的变体都塞进了「其他」。想象这个 `_` 是半年前写的、而 `Archived` 是上周加的 —— 编译器一句话都不会说。⇒ 判据:**`_` 只用在「未来新增的都该走这条」确实成立的地方**,否则宁可逐个写。
解构可以嵌套 —— 但分支顺序有意义
struct Point { x: i32, y: i32 }
enum Shape {
Circle { center: Point, r: i32 },
Line(Point, Point),
}
fn describe(s: &Shape) -> String {
match s {
Shape::Circle { center: Point { x: 0, y: 0 }, r } => format!("原点圆,半径 {r}"),
Shape::Circle { center, r } => format!("圆心 ({}, {}),半径 {r}", center.x, center.y),
Shape::Line(Point { x: x1, .. }, Point { x: x2, .. }) => format!("横跨 {x1} 到 {x2}"),
}
}
fn main() {
println!("{}", describe(&Shape::Circle { center: Point { x: 0, y: 0 }, r: 5 }));
println!("{}", describe(&Shape::Line(Point { x: 1, y: 2 }, Point { x: 9, y: 2 })));
}编译通过 · 输出 "原点圆,半径 5\n横跨 1 到 9\n"
对照:把「原点圆」那个**更具体**的分支挪到后面
struct Point { x: i32, y: i32 }
enum Shape {
Circle { center: Point, r: i32 },
Line(Point, Point),
}
fn describe(s: &Shape) -> String {
match s {
Shape::Circle { center, r } => format!("圆心 ({}, {}),半径 {r}", center.x, center.y),
Shape::Circle { center: Point { x: 0, y: 0 }, r } => format!("原点圆,半径 {r}"),
Shape::Line(Point { x: x1, .. }, Point { x: x2, .. }) => format!("横跨 {x1} 到 {x2}"),
}
}
fn main() {
println!("{}", describe(&Shape::Circle { center: Point { x: 0, y: 0 }, r: 5 }));
println!("{}", describe(&Shape::Line(Point { x: 1, y: 2 }, Point { x: 9, y: 2 })));
}编译通过,输出:"圆心 (0, 0),半径 5\n横跨 1 到 9\n"
模式可以一层层嵌下去:enum 变体里的 struct 字段里的字面量,一次匹配完。`..` 表示「剩下的字段不关心」。⚠️ 对照项**编译通过**(只有一条 unreachable 警告),但输出变了 —— **从上往下第一个匹配上的分支赢**,把宽的放前面会让窄的永远轮不上。
守卫:模式管形状,`if` 管取值
fn classify(n: i32) -> &'static str {
match n {
x if x < 0 => "负数",
0 => "零",
x if x % 2 == 0 => "正偶数",
_ => "正奇数",
}
}
fn main() {
println!("{} {} {} {}", classify(-1), classify(0), classify(4), classify(7));
}编译通过 · 输出 "负数 零 正偶数 正奇数\n"
对照:去掉 `if x % 2 == 0` 这个守卫
fn classify(n: i32) -> &'static str {
match n {
x if x < 0 => "负数",
0 => "零",
x => "正偶数",
_ => "正奇数",
}
}
fn main() {
println!("{} {} {} {}", classify(-1), classify(0), classify(4), classify(7));
}编译通过,输出:"负数 零 正偶数 正偶数\n"
守卫是模式之外的一个布尔条件,模式里表达不了的(比较、取模、调方法)都放这儿。⚠️ **守卫不参与穷尽性检查** —— 编译器不会去证明 `x < 0` 和 `x % 2 == 0` 合起来覆盖了所有情况,所以带守卫的 match 通常还是需要一个兜底分支。
`@` 绑定:既要判断范围,又要拿到值
fn check(n: i32) -> String {
match n {
small @ 1..=9 => format!("个位数 {small}"),
big @ 10..=99 => format!("两位数 {big}"),
other => format!("其他 {other}"),
}
}
fn main() {
println!("{} {} {}", check(5), check(42), check(1000));
}编译通过 · 输出 "个位数 5 两位数 42 其他 1000\n"
对照:把 `small @ 1..=9` 里的绑定去掉,只留 `1..=9`
fn check(n: i32) -> String {
match n {
1..=9 => format!("个位数 {small}"),
big @ 10..=99 => format!("两位数 {big}"),
other => format!("其他 {other}"),
}
}
fn main() {
println!("{} {} {}", check(5), check(42), check(1000));
}编译不过:error[E0425]
`1..=9` 这样的范围模式能判断,但判断完那个值就没名字了。`@` 的作用就是**一边判断一边起名**。对照项报 `E0425`:找不到叫 `small` 的东西。
`let ... else`:不匹配就走人
fn parse_port(s: &str) -> u16 {
let Ok(n) = s.parse::<u16>() else {
return 8080;
};
n
}
fn main() {
println!("{} {}", parse_port("3000"), parse_port("not a number"));
}编译通过 · 输出 "3000 8080\n"
对照:把 `else` 里的 `return 8080;` 改成一个值 `8080`
fn parse_port(s: &str) -> u16 {
let Ok(n) = s.parse::<u16>() else {
8080
};
n
}
fn main() {
println!("{} {}", parse_port("3000"), parse_port("not a number"));
}编译不过:error[E0308]
`let ... else` 解决的是「匹配上就继续用,匹配不上就提前退出」这个极常见的形状 —— 关键在于 `n` 在 `else` **之后**仍然可用,不像 `if let` 那样把它困在花括号里。⚠️ `else` 块必须**发散**(`return` / `break` / `continue` / `panic!`),对照项返回一个值就编译不过。