课程概览 · 第 4 章

上一篇:数组、切片和范围:固定长度、借用视图与区间边界

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

Rust 的 if、match、块和 loop 都可以产生值。控制流设计的目的不是消灭临时变量,而是让分支如何汇合、循环何时退出、失败如何返回在代码中清楚可见。

分支类型汇合、块尾分号、遍历所有权与早退检查
图:分支、分号、遍历来源与早退共同决定控制流和返回值。

学习目标

前置知识:绑定、基本类型、Option / Result 与切片。学完你能:

  • 用分支和块产生明确类型的值;
  • 根据尾表达式与分号定位意外的 ();
  • 按迭代来源与退出条件选择循环;
  • 用显式分支和命名错误完成输入校验。

本章只依赖标准库,采用 Edition 2024。独立示例各自放入 binary 项目的 src/main.rs,用 cargo +stable run 执行;故意失败例单独编译,不与正常例拼接。

if 产生一个值

if 整体只有一个类型,分支需要汇合为共同类型;常见情况是两个分支返回相同类型,也允许强制转换,如不返回的 ! 路径转换到另一分支的类型。不能仅凭表面写法不同就断定类型不匹配。需要表示缺值时用 Option<T>,不让魔法值同时承担“正常值”和“没有结果”。

独立示例 04-1(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn retry_delay_ms(attempt: u8) -> u64 {
if attempt == 0 {
0
} else {
100 * 2_u64.pow(u32::from((attempt - 1).min(5)))
}
}

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), 3200);
assert_eq!(discount(3), Some(20));
assert_eq!(discount(2), None);
}

尾表达式与分号

块尾的无分号表达式提供块的值。若最后一条是带分号的表达式语句且没有尾表达式,块的值是 ()。分号不是把原表达式内部的类型改掉,而是丢弃结果,影响所在块的返回值。let 是声明语句;赋值本身是值为 () 的表达式,不能把所有赋值统称为“不是表达式”。

故意失败:编译错误 04-2(标准库;文件:src/main.rs;尾分号使函数体为 (),与声明的 u64 不匹配,类别 E0308)。

1
2
3
4
5
6
7
8
9
10
11
fn retry_delay_ms(attempt: u8) -> u64 {
if attempt == 0 {
0
} else {
100_u64
};
}

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

去掉尾分号即可让 if 的结果作为返回值。也可以明确使用 return,但末尾没有特殊早退需求时通常保留尾表达式。

独立示例 04-3(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 mut count = 0;
let assigned: () = { count = 5 };
assert_eq!(assigned, ());
assert_eq!(count, 5);
}

let x = y = 5; 不会让 x 成为整数 5,赋值表达式给出的是 ()。第 1 章的 () 只有一个值,这与返回 Option::None 是不同语义。

match:结构分支与穷尽性

match 的分支同样汇合为共同类型,而且必须穷尽被匹配类型的所有情况。数值范围不能只覆盖示例输入就当作覆盖整个类型。业务状态会在第 8、9 章用枚举建模;本章先观察 u8 的覆盖。

独立示例 04-4(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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), "等待较长时间后重试");
}

删掉 _ 就遗漏 4..=255,编译器会拒绝非穷尽匹配。穷尽性检查证明覆盖类型的可能情况,不证明提示语符合业务需求。模式如何移动或借用数据留给第 9 章。

loop、while、for 与 break

需求写法值从哪里来
遍历已有序列for item in &items共享借用元素
修改已有序列for item in &mut items可变借用元素
按值遍历for item in itemsIntoIterator 决定,非 Copy 集合通常移动
条件为真时重复while condition不预设集合所有权
重复匹配成功值while let Some(x) = expr取决于 expr 返回拥有值还是引用
循环体内部决定结束loopbreak value 可以提供循环结果

while let 本身不保证消费集合:pop_front() 移出队列元素,iter.next() 则可以返回引用。for 是否消耗原容器也取决于迭代来源,例如 Copy 数组按值迭代是复制数组。

独立示例 04-5(标准库;文件:src/main.rs)。

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
48
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);

let mut queue = std::collections::VecDeque::from(["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());

let mut n = 0;
let first_multiple = loop {
n += 7;
if n % 5 == 0 {
break n;
}
};
assert_eq!(first_multiple, 35);

let mut remaining = 2;
while remaining > 0 {
remaining -= 1;
}
assert_eq!(remaining, 0);
let mut positives = 0;
for value in [-3, 4, -2, 9] {
if value < 0 {
continue;
}
positives += 1;
}
assert_eq!(positives, 2);
}

break 结束目标循环,continue 跳到下次迭代。普通 break 给 loop 提供 (),break value 可以产生其他结果;while、for 不支持这种带值退出。循环累加仍有整数溢出边界,本例只对给定小输入断言,不把控制流正确当作任意规模计算都正确。

守卫式校验:parse_limit

解析失败与业务拒绝是两种错误。String 错误消息适合展示,但不提供稳定的结构分支;本例以 LimitError 的变体区分“不为数值”和“零”。完整 Display / Error 实现在第 13 章设计,不在这里抢先引入。

独立示例 04-6:输入校验主例(标准库;文件:src/main.rs)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[derive(Debug, PartialEq, Eq)]
enum LimitError {
NotANumber,
Zero,
}

fn parse_limit(text: &str) -> Result<usize, LimitError> {
let value = match text.parse::<usize>() {
Ok(value) => value,
Err(_) => return Err(LimitError::NotANumber),
};
if value == 0 {
return Err(LimitError::Zero);
}
Ok(value)
}

fn main() {
assert_eq!(parse_limit("10"), Ok(10));
assert_eq!(parse_limit("abc"), Err(LimitError::NotANumber));
assert_eq!(parse_limit("0"), Err(LimitError::Zero));
assert_eq!(parse_limit("-1"), Err(LimitError::NotANumber));
}

解析成功分支提供 usize,失败分支用 return 退出,不需要提供继续执行的值,这也体现了分支汇合允许发散路径。随后用 if 拒绝 0,末尾只剩成功路径。这里的零限制是本例业务规则;超出 usize 范围的文本也归入 NotANumber,因为本例没有进一步细分解析错误。下一章在显式分支的基础上引入 ?。

边界提醒

表达式有类型,声明语句与表达式语句承担组织执行的角色,不能统一声称“语句的类型都是 ()”。需要嵌套时先判断是否真的存在层级关系,互斥状态通常更适合 enum + match。已证明的内部索引可以使用,遍历元素时则不必绕道索引;这些选择解决的是可读性与合同,不是风格上的绝对禁令。

自测

  1. if 分支表面类型不同就一定编译失败吗?
  2. 函数声明返回 u64,错误却说找到 (),应先检查什么?
  3. for item in items 与 while let 是否必定移动整个原容器?
  4. parse_limit("abc") 与 parse_limit("0") 如何区分?
  5. break value 可以在哪类循环中提供结果?

参考答案

  1. 不一定,分支须汇合为共同类型,允许相应强制转换,发散的 ! 路径也是例子。
  2. 检查函数体的尾表达式是否多了分号,或根本没有返回值。
  3. 不一定。所有权取决于迭代来源和被匹配表达式;非 Copy 集合按值迭代通常移动,while let 可以取得引用。
  4. 显式解析分支返回 NotANumber,零值守卫返回 Zero,调用方无需解析错误字符串。
  5. loop 支持带值 break;本章的 while、for 不支持。