用迭代器改写循环
本教程共 78 篇 · 第 50 篇 · 更新于 2026-08-08 · 约 10 分钟阅读
本节目标:把日常写的 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]);
collect 把 map 规划好的新序列,收集成 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。等熟悉后,再考虑用rayon把iter()换成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();
第二题:统计一个数组里偶数个数。循环要标志位或计数;迭代器用 filter 加 count:
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 改写成迭代器,可以按这张清单逐项对照,基本不会错:循环里有个累加变量、最后返回它 → 用 fold 或 sum / 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等价且零成本。
下一章专讲方法链式调用,把多个适配器串成一条流水线。