首页 / Rust 入门教程 / 用迭代器改写循环

Rust 入门教程

用迭代器改写循环

本教程共 78 篇 · 第 50 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程迭代器for 循环mapfiltersum声明式

本节目标:把日常写的 for 循环改写成迭代器风格,通过求和、变换、过滤、查找的对比,体会声明式写法的清晰与安全。

上一章讲了迭代器的基础。这一章动手把常见的 for 循环改写成迭代器,看看两种写法到底差在哪。

1-1 求和:循环 vs 迭代器

先做一个”把数组翻倍再求和”。命令式写法:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let mut sum = 0;
    for i in &v {
        sum += i * 2;
    }
    println!("{sum}"); // 30
}

你要操心:建个累加变量、写循环、手动加。改成迭代器:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let sum: i32 = v.iter().map(|x| x * 2).sum();
    println!("{sum}"); // 30
}

map(|x| x * 2) 描述”每个元素翻倍”,sum() 收尾求和。你只说了”想做什么”,没说”怎么一步步做”。这种风格叫声明式,比命令式更聚焦意图。

1-2 变换:把结果收成新集合

想把原数组每个元素平方,得到新数组。循环写法:

let v = vec![1, 2, 3];
let mut squared = Vec::new();
for i in &v {
    squared.push(i * i);
}

迭代器写法:

let v = vec![1, 2, 3];
let squared: Vec<_> = v.iter().map(|x| x * x).collect();
assert_eq!(squared, vec![1, 4, 9]);

collectmap 规划好的新序列,收集成 Vec。注意标 Vec<_>,让 collect 知道目标类型。

Note

map 返回的是新迭代器,原集合 v 一点没变。迭代器这个特性很舒服:处理过程中不破坏原始数据。

1-3 过滤:只留符合条件的

保留偶数,循环要写 if 判断再 push

let v = vec![1, 2, 3, 4, 5, 6];
let mut evens = Vec::new();
for i in &v {
    if i % 2 == 0 {
        evens.push(*i);
    }
}

filter 一步到位:

let v = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<_> = v.iter().filter(|x| *x % 2 == 0).collect();
assert_eq!(evens, vec![2, 4, 6]);

filter 的闭包接收的是引用 &i32,所以比较前要先解引用 *x。闭包返回 true 留下、false 剔除。

1-4 组合变换与过滤

要”先翻倍、再只留大于 5 的、最后求积”?循环里这些步骤全挤在一处:

let v = vec![1, 2, 3, 4];
let mut product = 1;
for i in &v {
    let doubled = i * 2;
    if doubled > 5 {
        product *= doubled;
    }
}
// doubled 值为 8(4*2),product = 8

迭代器把每个步骤拆成清晰的环节:

let v = vec![1, 2, 3, 4];
let product: i32 = v.iter()
    .map(|x| x * 2)
    .filter(|x| *x > 5)
    .product();
// 只有 8(来自 4*2)被保留,product = 8

map 翻倍、filter 筛掉不大于 5 的、product 把剩下的乘起来。每一环职责单一,读起来像顺着流水走。

1-5 查找:用 find 替代标志位

命令式找”第一个大于 3 的数”,常要设个标志变量:

let v = vec![1, 2, 3, 4, 5];
let mut found = None;
for i in &v {
    if *i > 3 {
        found = Some(*i);
        break;
    }
}

迭代器用 find,意图明明白白:

let v = vec![1, 2, 3, 4, 5];
let found = v.iter().find(|x| *x > 3);
assert_eq!(found, Some(&4));

find 返回 Option<&T>:找到了是 Some,没找到是 None。不用手动 break、不用标志变量,也不会漏写 break 导致多跑。

1-6 闭包能捕获环境

迭代器适配器接收的闭包,能像上一章讲的那样捕获外面的值。比如”只保留长度等于指定值的字符串”:

fn filter_by_len(words: Vec<String>, target: usize) -> Vec<String> {
    words.into_iter()
        .filter(|w| w.len() == target)
        .collect()
}

闭包 |w| w.len() == target 用到了参数 target,这就是闭包捕获环境的能力——普通函数做不到。

1-7 两种写法怎么选

命令式 for 循环和迭代器,结果完全等价。新手可能更习惯 for,这没关系。但迭代器有几个实在的好处:

  • 少出错:不用手动管索引、累加变量、break,边界检查由编译器优化掉;
  • 更聚焦:每个环节只说”做什么”,逻辑一眼看清;
  • 零成本:迭代器编译后和手写循环一样快(详见下一章)。
Tip

不必强迫自己一开始就用迭代器。先把 for 写顺,再试着改写成 map/filter/collect,慢慢就更爱用迭代器了。两种都会,才是真熟。

1-8 什么时候别硬改成迭代器

迭代器很香,但不是所有循环都该被它取代。判断标准是”可读性优先”。如果一个循环里夹着多个互相纠缠的状态变量、复杂的提前退出条件、或依赖前几次迭代的副作用,硬写成 map/fold 链反而让人看不懂——这时保留清晰的 for 循环更好。

另一个现实考量是:迭代器链一旦很长,出错时栈回溯不如普通循环直观;遇到性能敏感且编译器没优化好的极端情况,手写循环反而可控。Rust 哲学是”零成本抽象”,但”零成本”不等于”永远最可读”,你才是最终裁判。

Tip

入门阶段建议:能用迭代器清晰表达时就用它(求和、变换、过滤、查找都很合适);一旦为了凑链式而写出绕弯的代码,就退回到 for。等熟悉后,再考虑用 rayoniter() 换成 par_iter() 轻松并行化——那是后话。

1-9 两道完整对比题

光讲原则不如上手比。第一题:求 1 到 5 的平方和。循环写法要一个累加变量;迭代器写法一行收尾。

// 循环
let mut sum = 0;
for i in 1..=5 { sum += i * i; }

// 迭代器
let sum: i32 = (1..=5).map(|i| i * i).sum();

第二题:统计一个数组里偶数个数。循环要标志位或计数;迭代器用 filtercount

let v = [1, 2, 3, 4, 5, 6];
let even = v.iter().filter(|&&x| x % 2 == 0).count(); // 3

对比能看出:迭代器把”做什么”说清楚,循环把”怎么做”说清楚。当逻辑是”一串变换后给个结果”时,迭代器往往更直白;当逻辑满是分支和副作用时,循环更稳妥。两种都合法,挑读起来顺手的。

Note

别陷入”必须全改成迭代器”的执念。Rust 不打分,可读性才是第一生产力;熟练之后你会自然知道哪题该用哪种。

1-10 早停与条件截取

除了”全部处理完”,迭代器也很擅长”处理到某条件就收手”。take_while(条件) 会一直取元素,直到条件第一次为假就停;find(条件) 找第一个满足的;position(条件) 返回第一个满足的下标——这些都不必遍历剩余元素,天然带”早停”。

let v = [1, 2, 3, 4, 5, 1];
let prefix: Vec<i32> = v.iter().copied().take_while(|&x| x < 4).collect();
// 遇到 4 停下 -> [1, 2, 3]

对比手写 for 里加 if 条件 { break; },迭代器把”早停条件”直接写在链上,意图更明显。但要注意:如果”早停”依赖复杂的跨次状态、或停下后还要做一堆收尾副作用,那 for 循环反而更直白。

Tip

把循环改写成迭代器时,先看它”是不是处理全部、还是处理到某条件”。前者用 map/filter/fold 顺手,后者用 take_while/find/position 更贴切;都不对路就保留 for

1-11 从循环迁移到迭代器的清单

想把手头的 for 改写成迭代器,可以按这张清单逐项对照,基本不会错:循环里有个累加变量、最后返回它 → 用 foldsum / product;循环在”只保留符合条件的” → 用 filter;循环在”对每个元素变换” → 用 map;循环在”找第一个/带下标” → 用 find / enumerate;循环在”处理到某条件就停” → 用 take_while / position

剩下的那些:状态在多次迭代间纠缠、分支一堆、还顺手改了外部好几个变量——这类就别硬改,保留 for 反而最清楚。迁移不是目的,可读性才是。

// 清单对照:累加平方 -> fold
let v = [1, 2, 3];
let s: i32 = v.iter().map(|x| x * x).sum();
Note

改写后若链子超过三四个环节或闭包开始复杂,就按上一章说的把它抽成有名字的函数,别让一行代码承担太多意图。

1-12 小结

  • 求和:iter().map(...).sum() 替代累加变量;
  • 变换收集合:iter().map(...).collect() 替代 push 循环;
  • 过滤:filter(...) 替代 if + push
  • 组合多步:每个环节一个适配器,逻辑清晰;
  • 查找:find(...) 返回 Option,省去标志变量和 break
  • 适配器闭包能捕获环境值;迭代器与 for 等价且零成本。

下一章专讲方法链式调用,把多个适配器串成一条流水线。