课程概览 · 第 4 章

上一篇:切片:为函数设计稳定、低耦合的输入

下一篇:函数与 I/O:把所有权、错误和资源边界写进接口

Rust 是表达式语言:ifmatch、代码块乃至循环都能产生值。这让"从输入算出一个结果"可以写成一条类型明确的表达式链。但工程上的优先级不是炫技式地消灭临时变量,而是让每个分支的类型、每个循环的退出条件、每条失败路径在评审时一眼可见。

if 和 match 的类型汇合、分号产生单位值、for 遍历所有权与 let else 早退的审查清单
图:表达式、分号、遍历和早退共同决定控制流是否清楚、值是否被正确保留。

学习目标与默认选择

学完本章你能:

  • if / match / 块作为表达式产生一个确定类型的值;
  • 解释尾分号如何把表达式变成 (),并从编译器报错定位这类 bug;
  • 按所有权与退出条件在 for / while / loop / while let 之间选择;
  • 用命名错误枚举(而不是 String)写守卫式的输入校验。

默认选择:

需求写法
二选一的值if 表达式(分支类型必须一致)
多分支、按结构分解match(穷尽性由编译器保证)
“可能没有结果”返回 Option<T>,不用魔法值
“可能失败”返回 Result<T, E>E 是命名类型(本章末尾,第 13 章展开)
遍历集合for item in &items(默认借用)

if 产生一个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// 第 attempt 次重试后的等待毫秒数。指数退避,封顶 5 次幂。
fn retry_delay_ms(attempt: u8) -> u64 {
if attempt == 0 {
0
} else {
100 * 2_u64.pow((attempt - 1).min(5) as u32)
}
}

/// 等级达到 3 才有折扣;没有折扣是"没有值",不是 0。
fn discount(level: u8) -> Option<u8> {
if level >= 3 { Some(20) } else { None }
}

fn main() {
assert_eq!(retry_delay_ms(0), 0);
assert_eq!(retry_delay_ms(1), 100);
assert_eq!(retry_delay_ms(2), 200);
assert_eq!(retry_delay_ms(4), 800);
assert_eq!(retry_delay_ms(50), 100 * 32); // 封顶后不再翻倍

assert_eq!(discount(3), Some(20));
assert_eq!(discount(9), Some(20));
assert_eq!(discount(2), None);
}

两个分支的类型必须一致:整个 if 只产生一个确定类型的值。需要表达"没有值"时,让两个分支都是 Option<T>Some(...) / None),不要用 -10 这类魔法值冒充。

尾分号:表达式如何变成 ()

块的最后一条无分号表达式是块的值;加上分号后它变成语句,块的值变成 ()。这是"函数意外返回单元类型"的头号原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
无法编译:函数声明返回 u64,但 if 表达式的值被尾分号变成了 ()。

fn retry_delay_ms(attempt: u8) -> u64 {
if attempt == 0 {
0
} else {
100_u64
}; // <- 尾分号:整个块变成语句,值为 ()
}

fn main() {
let _ = retry_delay_ms(1);
}

编译器拒绝原因(E0308):
error[E0308]: mismatched types
--> src/main.rs:2:35
|
2 | fn retry_delay_ms(attempt: u8) -> u64 {
| -------------- ^^^ expected `u64`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
...
7 | }; // <- 尾分号:整个块变成语句,值为 ()
| - help: remove this semicolon to return this value

修正版–去掉尾分号,块产生 u64

1
2
3
4
5
6
7
8
9
10
11
12
fn retry_delay_ms(attempt: u8) -> u64 {
if attempt == 0 {
0
} else {
100_u64
} // 尾表达式无分号:这就是返回值
}

fn main() {
assert_eq!(retry_delay_ms(0), 0);
assert_eq!(retry_delay_ms(1), 100);
}

顺带一提,赋值表达式本身也是 (),所以 let x = y = 5; 不是链式赋值。这不是需要背诵的规则,而是同一条原理:语句丢弃值,表达式产生值

match:多分支按结构产生一个值

match 的每个分支都产生同类型值,且必须穷尽所有模式。匹配业务互斥状态时,穷尽性检查是免费的评审员–第 8、9 章会把它用在 TaskState 上,本章先用数值范围感受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 把重试次数映射成用户可读的提示语。
fn wait_message(attempt: u8) -> &'static str {
match attempt {
0 => "立即执行",
1..=3 => "等待重试",
_ => "等待较长时间后重试",
}
}

fn main() {
assert_eq!(wait_message(0), "立即执行");
assert_eq!(wait_message(2), "等待重试");
assert_eq!(wait_message(9), "等待较长时间后重试");
}

删掉 _ 分支试试:编译器直接报 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// 累加到第一个负数为止(不包含该负数)。
fn sum_until_negative(values: &[i32]) -> i32 {
let mut total = 0;
for &value in values {
if value < 0 {
break;
}
total += value;
}
total // 尾表达式:函数返回值
}

fn main() {
assert_eq!(sum_until_negative(&[10, 20, -5, 30]), 30);
assert_eq!(sum_until_negative(&[1, 2, 3]), 6);
assert_eq!(sum_until_negative(&[]), 0);
assert_eq!(sum_until_negative(&[-1, 5]), 0);

// while let:逐个消费队列元素,取空即退出。
let mut queue = std::collections::VecDeque::from(vec!["a", "b", "c"]);
let mut drained = Vec::new();
while let Some(item) = queue.pop_front() {
drained.push(item);
}
assert_eq!(drained, vec!["a", "b", "c"]);
assert!(queue.is_empty()); // 元素所有权已转移到 drained

// loop + break value:循环本身产生一个值。
let mut n = 0;
let first_multiple = loop {
n += 7;
if n % 5 == 0 {
break n; // break 带值:loop 的结果
}
};
assert_eq!(first_multiple, 35);

// continue:跳过本次迭代,循环继续。
let mut positives = 0;
for &v in [-3, 4, -2, 9].iter() {
if v < 0 {
continue;
}
positives += 1;
}
assert_eq!(positives, 2);
}

默认用 for 遍历;只有在真正需要索引时用 enumerate(),而不是 for i in 0..vec.len()loop 只用于重试、状态机这类"退出点在循环体中间"的场景。

守卫式校验:parse_limit 与命名错误

校验用户输入时,"失败原因"是业务信息,返回 Result<usize, String> 会丢掉类型、迫使调用方解析字符串。先定义一个小的错误枚举,用 match 把失败变成可分支的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// 解析 limit 参数的失败原因。
#[derive(Debug, PartialEq, Eq)]
enum LimitError {
NotANumber,
Zero,
}

/// 解析并校验 limit:必须是正整数。
/// "0" 被拒绝:limit 为 0 会让分页/重试逻辑失去意义。
fn parse_limit(text: &str) -> Result<usize, LimitError> {
let value: usize = text.parse().map_err(|_| LimitError::NotANumber)?;
if value == 0 {
return Err(LimitError::Zero);
}
Ok(value)
}

fn main() {
// 成功路径。
assert_eq!(parse_limit("10"), Ok(10));

// 失败路径 1:不是数字。
assert_eq!(parse_limit("abc"), Err(LimitError::NotANumber));

// 失败路径 2:零被显式拒绝。
assert_eq!(parse_limit("0"), Err(LimitError::Zero));
}

守卫式写法(早返回)让主流程保持直线:解析失败立即退出,零值立即退出,读到最后一行时剩下的就是成功路径。嵌套的 if 校验会把这个结构倒过来。map_errParseIntError 转换成本函数的错误枚举;?Err 时提前返回–下一章会把 ? 与 I/O 资源清理一起讲透。

为什么可行:语句丢弃值,表达式产生值

赋值、let 绑定、带分号的调用是语句,类型为 ();字面量、函数调用、ifmatch、块、循环可以是表达式。函数体最后一个无分号表达式即返回值。这个规则带来两个直接后果:其一,分支无需临时变量,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 章),穷尽性检查会替你兜底。

自测

  1. 为什么 if 的两个分支必须同类型?–答方向:if 是表达式,整体只有一个类型;分支类型汇合处就是类型检查点。
  2. 函数声明返回 u64 但报 E0308 found (),第一个检查什么?–答方向:块尾表达式是否被分号降级成了语句。
  3. for item in itemsfor item in &itemsfor item in &mut items 对集合分别做了什么?–答方向:消耗(移动)、只读借用、独占可变借用;第一种之后集合不可再用。
  4. parse_limit 为什么拒绝 "0" 且不用 String 错误?–答方向:limit 为 0 在业务上无意义;LimitError::Zero 让调用方按变体分支处理,String 只能匹配文本。
  5. break value 和普通 break 的差别是什么?–答方向:前者让 loop 表达式产生一个值,后者产生 ();适合"循环直到算出结果"的场景。