表达式与控制流:让分支、循环和返回值清楚可审查
课程概览 · 第 4 章
Rust 是表达式语言:if、match、代码块乃至循环都能产生值。这让"从输入算出一个结果"可以写成一条类型明确的表达式链。但工程上的优先级不是炫技式地消灭临时变量,而是让每个分支的类型、每个循环的退出条件、每条失败路径在评审时一眼可见。
学习目标与默认选择
学完本章你能:
- 用
if/match/ 块作为表达式产生一个确定类型的值; - 解释尾分号如何把表达式变成
(),并从编译器报错定位这类 bug; - 按所有权与退出条件在
for/while/loop/while let之间选择; - 用命名错误枚举(而不是
String)写守卫式的输入校验。
默认选择:
| 需求 | 写法 |
|---|---|
| 二选一的值 | if 表达式(分支类型必须一致) |
| 多分支、按结构分解 | match(穷尽性由编译器保证) |
| “可能没有结果” | 返回 Option<T>,不用魔法值 |
| “可能失败” | 返回 Result<T, E>,E 是命名类型(本章末尾,第 13 章展开) |
| 遍历集合 | for item in &items(默认借用) |
if 产生一个值
1 | |
两个分支的类型必须一致:整个 if 只产生一个确定类型的值。需要表达"没有值"时,让两个分支都是 Option<T>(Some(...) / None),不要用 -1、0 这类魔法值冒充。
尾分号:表达式如何变成 ()
块的最后一条无分号表达式是块的值;加上分号后它变成语句,块的值变成 ()。这是"函数意外返回单元类型"的头号原因:
1 | |
修正版–去掉尾分号,块产生 u64:
1 | |
顺带一提,赋值表达式本身也是 (),所以 let x = y = 5; 不是链式赋值。这不是需要背诵的规则,而是同一条原理:语句丢弃值,表达式产生值。
match:多分支按结构产生一个值
match 的每个分支都产生同类型值,且必须穷尽所有模式。匹配业务互斥状态时,穷尽性检查是免费的评审员–第 8、9 章会把它用在 TaskState 上,本章先用数值范围感受:
1 | |
删掉 _ 分支试试:编译器直接报 non-exhaustive match。这就是穷尽性–分支覆盖是类型系统的要求,不是评审时的自觉。
循环:所有权与退出条件
| 需求 | 写法 | 所有权角色 |
|---|---|---|
| 遍历集合,不消耗它 | for item in &items | 只读借用 |
| 修改元素 | for item in &mut items | 独占可变借用 |
| 取走每个元素 | for item in items | 消耗集合,之后不可再用 |
| 明确条件重复 | while cond | 与集合无关 |
| 消费迭代器/队列直到取空 | while let Some(x) = it.next() | 逐个取得所有权 |
| 无限循环 + 内部退出点 | loop { ... break; } | break value 可让 loop 产生值 |
1 | |
默认用 for 遍历;只有在真正需要索引时用 enumerate(),而不是 for i in 0..vec.len()。loop 只用于重试、状态机这类"退出点在循环体中间"的场景。
守卫式校验:parse_limit 与命名错误
校验用户输入时,"失败原因"是业务信息,返回 Result<usize, String> 会丢掉类型、迫使调用方解析字符串。先定义一个小的错误枚举,用 match 把失败变成可分支的值:
1 | |
守卫式写法(早返回)让主流程保持直线:解析失败立即退出,零值立即退出,读到最后一行时剩下的就是成功路径。嵌套的 if 校验会把这个结构倒过来。map_err 把 ParseIntError 转换成本函数的错误枚举;? 在 Err 时提前返回–下一章会把 ? 与 I/O 资源清理一起讲透。
为什么可行:语句丢弃值,表达式产生值
赋值、let 绑定、带分号的调用是语句,类型为 ();字面量、函数调用、if、match、块、循环可以是表达式。函数体最后一个无分号表达式即返回值。这个规则带来两个直接后果:其一,分支无需临时变量,let label = if cond { "a" } else { "b" }; 一行完成;其二,两个分支必须同类型,因为整个 if 只有一个类型。loop 之所以也能产生值(break value),是同一原理的延伸:控制流结构本身是表达式,除非你用分号把它降级成语句。
常见误区
- 尾分号导致返回
():报错 E0308 指向函数结尾时,先检查块的最后一条表达式是不是多了分号。 - 用魔法值表达"没有结果":
-1表示失败、0表示无折扣–调用方无从区分"值恰为 0"和"没有值";用Option。 for value in values之后还用values:集合已被消耗(移动);要继续用就&values。- 为拿索引写
for i in 0..len():直接迭代更安全;确需位置用.enumerate()。 - 错误用
String传递:调用方只能匹配字符串内容,编译器帮不上忙;用命名枚举(本章LimitError,完整展开见第 13 章)。 - 嵌套
if表达互斥业务状态:那是enum + match的场景(第 8、9 章),穷尽性检查会替你兜底。
自测
- 为什么
if的两个分支必须同类型?–答方向:if是表达式,整体只有一个类型;分支类型汇合处就是类型检查点。 - 函数声明返回
u64但报 E0308 found(),第一个检查什么?–答方向:块尾表达式是否被分号降级成了语句。 for item in items、for item in &items、for item in &mut items对集合分别做了什么?–答方向:消耗(移动)、只读借用、独占可变借用;第一种之后集合不可再用。parse_limit为什么拒绝"0"且不用String错误?–答方向:limit 为 0 在业务上无意义;LimitError::Zero让调用方按变体分支处理,String只能匹配文本。break value和普通break的差别是什么?–答方向:前者让loop表达式产生一个值,后者产生();适合"循环直到算出结果"的场景。






