控制流:if、loop、while 与 for

控制流是任何语言的必修课,Rust 的版本在语法上小巧紧凑,但细节处有它自己的主张:if 是表达式、loop 可以返回值、for 默认迭代而非索引、break 支持 labelled 跳出多层。这篇解决的问题是:四种循环与分支的精确语义、它们与所有权规则的配合方式、以及从其他语言迁移时需要修正的直觉。读完你写循环时不会再下意识地用索引加长度这种 C 风格写法。

一、if:表达式而非语句

Rust 的 if 与 C 家族最大差别是它能产生产值,因此可以直接用在赋值右侧:

fn main() {
    let n = 7;
    let parity = if n % 2 == 0 { "偶" } else { "奇" };
    println!("{}", parity);
}

两个分支必须产出同一类型,否则编译失败。这个约束把「分支一致性」从代码评审里搬进了编译器——你不可能再不小心让 if 分支返回字符串、else 分支返回数字。另外条件必须是严格的 bool,if n、if n != 0 之外的隐式真值转换一律拒绝,配合 if let 与 match,Rust 的条件表达几乎不存在歧义写法。

顺带一提,if let 是 if 的轻量亲戚,用在「只关心一种可能」时:if let Some(v) = opt 比 match 写一大堆 _ => {} 干净得多。它同样可以是表达式(若分支产出值),但多数场合只用来做条件解包。等到枚举与 Option 一章,你会发现 if let、match、while let 三者共享同一套模式语法,学一得三。

二、loop:自带返回值的无限循环

loop 是无条件循环,需要 break 退出。它的特殊之处在于 break 可以带值,从而让整个循环成为表达式:

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;   // 循环产出 20
        }
    };
    println!("{}", result);
}

这种写法在「重试直到成功」的场景很自然:把循环结果作为返回值,逻辑紧凑且不用额外变量传值。while 则先判条件后执行,while let 组合模式匹配做循环退出,处理流式输入时常见。三种循环的一览如下表。

形式 退出方式 产出值 典型用途
loop break(可带值) break 的值 重试、事件循环
while cond 条件为假 无 条件轮询
while let pat = expr 模式不匹配 无 流式消费
for x in iter 迭代器耗尽 无 遍历集合
if cond 不适用 分支值 分支选择

三、for:迭代而不是索引

Rust 的 for 循环语义是「对迭代器逐项处理」,而不是「从 i 到 j 按步长递增」。遍历集合的规范写法:

fn main() {
    let names = vec!["张三", "李四", "王五"];
    for name in &names {              // 借用遍历
        println!("你好,{}", name);
    }
    for (i, name) in names.iter().enumerate() {
        println!("{}: {}", i, name);
    }
}

三种遍历形态要分清:for x in &v 拿到 &T(只读借用),for x in &mut v 拿到 &mut T(可变借用),for x in v 直接消耗 v(move,仅在 v 按值可用时合法)。编译器在每种形态下都保证借用规则,所以「边遍历边修改整个集合」天然编译不过,正确做法见迭代器一章。

需要按字节范围计数时用 for i in 0..n,注意这里 0..n 与切片区间一样前闭后开;想包含端点用 0..=n。从 Python 转来的同学对 range 语义会很熟悉,从 C 转来的同学要改掉用 i < len 手写条件的反射。

for 循环还有一个隐藏优点:它配合 ownership 天然安全。按值消耗型 for(for x in v)在循环结束后容器正好被腾空,不会出现「迭代到一半容器被修改」的悬挂引用。反过来说,当你确实需要在遍历时修改集合,编译器会逼你选 iter_mut 或先收集再修改,这正是借用检查器在循环场景的工作方式。

四、break、continue 与标签

多层循环里想一次性跳出,Rust 用生命周期标签解决:

fn main() {
    'outer: for i in 0..3 {
        for j in 0..3 {
            if i == 1 && j == 1 {
                break 'outer;        // 直接跳出外层
            }
            println!("{} {}", i, j);
        }
    }
}

标签以单引号开头、冒号结尾,写在循环之前,break 'label 与 continue 'label 都能引用。写矩阵搜索、网格游戏、解析嵌套结构时这是唯一干净的早退方式,比靠标志位层层判断优雅得多。

五、控制流的所有权细节

循环与所有权有三个交互点值得注意。其一,for 循环里的迭代器借用持续整个循环,循环体内修改被遍历的集合会触发借用冲突,编译错误形如 cannot borrow as mutable because it is also borrowed as immutable。其二,loop 体内的 let 变量每轮重新创建、轮末释放,不存在「上一轮的值泄漏到下一轮」的悬空问题。其三,break 带值的 move 与普通返回一样受所有权约束,迭代器耗尽时底层集合按原所有权处理。

fn main() {
    let mut nums = vec![1, 2, 3];
    for n in &mut nums {
        *n *= 2;                 // 原地加倍
    }
    println!("{:?}", nums);      // [2, 4, 6]
}

这段合法的原因是 &mut 迭代器每轮只借出一个元素,冲突不存在。等学完迭代器再回看这条解释会更加清晰。

六、从其他语言迁移的修正清单

旧习惯 Rust 写法 修正原因
for (i=0; i<n; i++) 索引遍历 for x in &v 索引有边界检查成本
循环里累积 result 变量 用迭代器链 collect 零成本抽象更地道
用 flag 变量跳出多层 break 加标签 标签语义明确
if 分支返回不同类型 统一分支类型 类型系统硬性要求
while 手动移动迭代器游标 for 消费迭代器 借用规则更少冲突

第一条值得单独强调:Rust 的索引访问带运行时边界检查,而迭代器在编译期已知范围, release 构建下会被优化成等价的直接遍历。养成迭代器偏好既是为了安全也是为了性能。做文本处理工具时这套写法收益最大,正则表达式精进 的示例几乎全部用迭代器链表达处理流程。若想深入了解编译器如何呈现循环优化结果,也可以配合 Claude Code 使用指南 的分析工作流检查生成的汇编差异。

延伸阅读

  • 下一篇「模式匹配」介绍 if let、match 的完整形态,与本篇的循环控制天然衔接。
  • 迭代器一章展开 map、filter、collect 三件套,是循环表达的完全体,读完那里本篇的所有示例都可以再精简一遍。
  • 桌面 UI 的事件循环就是 loop 加 channel 的组合,Tauri 桌面开发指南 里有可运行范例,与本篇的 loop 语义直接对应,对照阅读能更快建立直觉。