一句话总结
迭代器(Iterator)把"遍历 + 变换 + 聚合"组合成链式调用.
它的两个关键性质是:惰性(适配器不立即执行,直到消费器触发)和零成本(编译后等价于手写循环).
掌握迭代器是写出地道,高效 Rust 的分水岭.
一个对比:迭代器 vs 手写循环
求"偶数的平方和".Go 的写法:
sum := 0 for _, n := range nums { if n%2 == 0 { sum += n * n } }
|
Rust 的迭代器写法:
let sum: i32 = nums.iter() .filter(|&&n| n % 2 == 0) .map(|&n| n * n) .sum();
|
意图("筛选 → 变换 → 聚合")直接写在代码结构里,无需手动维护累加变量和索引.这种声明式风格读起来更接近"做什么"而非"怎么做".
适配器与消费器
迭代器方法分两类,这是理解迭代器的关键划分:
| 类别 |
作用 |
例子 |
| 适配器(adapter) |
把一个迭代器变换成另一个迭代器,惰性 |
map,filter,enumerate,zip,take,skip |
| 消费器(consumer) |
真正驱动迭代,产出最终结果 |
sum,collect,count,for_each,fold,find |
一条迭代器链通常是"若干适配器 + 一个消费器".适配器层层叠加描述变换,最后由消费器一次性驱动整条链运转.
关键性质一:惰性求值
适配器不会立即执行,只是记录"将要做什么".直到遇到消费器,整条链才真正运转:
let iter = nums.iter().map(|&n| { println!("处理 {n}"); n * 2 });
let result: Vec<i32> = iter.collect();
|
忘记消费器 = 什么都没做
只写 nums.iter().map(...) 而不接 collect/sum/for_each,这段代码什么也不会发生,编译器还会给 unused must_use 警告.
务必让迭代器链以一个消费器收尾.这是新手最常见的"代码没生效"困惑之一.
关键性质二:零成本抽象
担心这么多层链式调用会拖慢性能?不会.编译器会把迭代器链优化成与手写 for 循环等价的机器码,没有额外的函数调用或临时集合开销.
迭代器是 Rust "零成本抽象"理念的招牌:写着高层,声明式的链式代码,编译后却和亲手优化的底层循环一样快.
鱼与熊掌兼得--可读性来自源码,性能来自编译器.Go 的 for range 性能虽好,但缺少这套可组合的链式表达力.
三种迭代:iter,iter_mut,into_iter
取得迭代器有三种方式,差别在对元素的所有权--直接呼应第 03,04 篇:
| 方法 |
产出元素类型 |
对原集合的影响 |
iter() |
&T(只读借用) |
不变,仍可用 |
iter_mut() |
&mut T(可变借用) |
可原地修改元素 |
into_iter() |
T(取得所有权) |
消耗集合,之后不可用 |
let names = vec![String::from("a"), String::from("b")]; let upper: Vec<String> = names.into_iter() .map(|s| s.to_uppercase()) .collect();
|
for 循环就是迭代器的语法糖
第 07 篇的三种 for 遍历(&v / &mut v / v),底层正是 iter() / iter_mut() / into_iter().
for x in &v 等价于 for x in v.iter().理解了这层对应,for 循环与迭代器链就统一起来了.
高频方法巡礼
前文的分类表格给了全局视角,下面按适配器和消费器分别过一遍最常用的方法,每个附最短的可运行示例.最后给出几个日常组合模式.
常用适配器
适配器都是惰性的--单独写不触发任何计算,只在消费器驱动时才真正执行.
let v = vec![10, 20, 30, 40, 50];
let indexed: Vec<_> = v.iter().enumerate().collect();
let names = vec!["a", "b", "c"]; let paired: Vec<_> = v.iter().zip(names.iter()).collect();
let first3: Vec<_> = v.iter().take(3).collect(); let tail: Vec<_> = v.iter().skip(2).collect();
let squares: Vec<i32> = v.iter().cloned().map(|x| x * x).collect();
|
常用消费器
消费器驱动整条链,产出最终值.一条完整的迭代器链必须有一个消费器收尾.
let total: i32 = v.iter().sum(); let factorial = (1..=5).product::<i32>();
let how_many = v.iter().filter(|&&x| x > 25).count();
let sum = v.iter().fold(0, |acc, &x| acc + x); let prod = v.iter().fold(1, |acc, &x| acc * x); let max = v.iter().fold(i32::MIN, |acc, &x| acc.max(x));
let csv = v.iter().fold(String::new(), |mut acc, x| { if !acc.is_empty() { acc.push(','); } acc.push_str(&x.to_string()); acc });
v.iter().for_each(|x| println!("{x}"));
let first = v.iter().find(|&&x| x > 25);
let has_big = v.iter().any(|&&x| x > 100); let all_pos = v.iter().all(|&&x| x > 0);
let biggest = v.iter().max();
let third = v.iter().nth(2);
|
fold 与 for_each 的区别
fold 返回累加结果,适合"汇总成某个值";for_each 返回 (),仅做副作用.
日常遍历打印直接用 for ... in ... 即可,不必强行 for_each.
消费器之重:collect
collect 是迭代器链最常用的终点--把"流"收拢为具体的集合.它的签名:
fn collect<B: FromIterator<Self::Item>>(self) -> B
|
关键在于 B 可以是任何实现了 FromIterator 的类型.同一个迭代器链,collect 的目标类型不同,结果就不同.
收集为常见集合
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let names = vec!["a", "b", "c"]; let map: HashMap<_, _> = names.iter().enumerate().map(|(i, &n)| (n, i)).collect();
let nums = vec![1, 2, 2, 3, 3, 3]; let unique: HashSet<_> = nums.into_iter().collect();
let chars = vec!['h', 'e', 'l', 'l', 'o']; let s: String = chars.iter().collect();
|
类型标注的三种方式
因为 collect 的返回类型是泛型参数,编译器需要知道目标类型.有三种告知方式:
let v = iter.collect::<Vec<_>>();
let v: Vec<i32> = iter.collect();
|
Result 与 Option 的 collect 魔法
这是 collect 最实用的特殊能力:当迭代器元素是 Result 或 Option 时,collect 可以"反过来"--把 Vec<Result<T, E>> 收成 Result<Vec<T>, E>,遇到第一个 Err 就短路.
let inputs = vec!["1", "2", "three", "4"];
let all_ok: Result<Vec<i32>, _> = inputs.iter() .map(|s| s.parse::<i32>()) .collect();
let nums = vec![Some(1), Some(2), None, Some(4)]; let all_some: Option<Vec<i32>> = nums.into_iter().collect();
|
本质是 Rust 为标准库的 Result 和 Option 分别实现了 FromIterator,逻辑就是"逐元素检查,全成功才产出 Vec,遇失败立即短路".这个模式在解析,校验,批量 IO 场景极常见.
collect 后的所有权
结合前面的知识,collect 产出的集合拥有其元素.迭代时要注意:
let names = vec![String::from("a"), String::from("b")];
let owned: Vec<String> = names.into_iter().collect();
let names = vec![String::from("a"), String::from("b")];
let borrowed: Vec<&String> = names.iter().collect();
let cloned: Vec<String> = names.iter().cloned().collect();
|
常见组合模式
单个方法威力有限,链式组合才是迭代器的真正优势.下面几种模式在实际代码中出现频率极高.
let pos = v.iter().enumerate() .find(|(_, &x)| x > 25) .map(|(i, _)| i);
let page = 2; let page_size = 3; let page_data: Vec<_> = v.iter() .skip((page - 1) * page_size) .take(page_size) .collect();
let nested = vec![vec![1, 2], vec![3, 4, 5]]; let flat: Vec<_> = nested.iter().flatten().collect(); let doubled: Vec<_> = (0..3).flat_map(|x| [x, x * 2]).collect();
use std::collections::HashMap; let words = vec!["apple", "ant", "banana", "bat"]; let by_first = words.iter().fold(HashMap::new(), |mut map, w| { map.entry(w.chars().next().unwrap()).or_insert(vec![]).push(w); map });
let inputs = vec!["1", "two", "3", "four"]; let nums: Vec<i32> = inputs.iter() .filter_map(|s| s.parse::<i32>().ok()) .collect();
|
最佳实践:优先迭代器链,而非手写索引循环
能用 iter().map().filter().collect() 表达的,就不要写 for i in 0..v.len() 加手动索引--前者更不易出错(无越界风险),更易读,且同样快.
Clippy 会主动建议把"索引循环"改成迭代器写法.这是 Rust 代码地道与否的一个明显标志.
快速回顾
- 两类方法:适配器(map/filter/...,惰性)+ 消费器(collect/sum/...,驱动执行).
- 惰性:没有消费器,适配器链什么都不做(注意 must_use 警告).
- 零成本:迭代器链编译为与手写循环等价的机器码,无性能损失.
- 三种迭代:
iter(借用)/iter_mut(可变借用)/into_iter(取所有权);for 循环即其语法糖.
- 高频方法:适配器(enumerate,zip,take,skip,cloned)与消费器(fold,find,any/all,max,nth).
- collect:重中之重,可产出 Vec/HashMap/HashSet/String 等集合,还能翻转 Result/Option 迭代器.
动手练习
- 链式求值:用迭代器链求一个向量中所有奇数的平方和.
- 惰性验证:写一个只有适配器,没有消费器的迭代器链,观察编译器的 unused 警告,再补上
collect 修复.
- enumerate 与 zip:用
enumerate 打印带序号的列表;用 zip 把两个等长向量配对成 Vec<(_, _)>.
- fold 与 collect:用
fold 实现求阶乘;用 into_iter().map().collect() 把一个 Vec<String> 全转大写.
- 索引循环改写:把一段
for i in 0..v.len() + 索引访问的代码改写成迭代器链,对比两者可读性.