阶段四 · 集合与函数式

迭代器

一句话总结

迭代器(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(); // 此刻才逐元素执行 map

忘记消费器 = 什么都没做

只写 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()) // s 是 String 本身,可随意消费
.collect();
// names 已被消耗,这里不能再用

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];

// enumerate: 为每个元素附上索引 (usize)
let indexed: Vec<_> = v.iter().enumerate().collect();
// [(0, &10), (1, &20), (2, &30), (3, &40), (4, &50)]

// zip: 将两个迭代器逐对拉链,较短的耗尽即停
let names = vec!["a", "b", "c"];
let paired: Vec<_> = v.iter().zip(names.iter()).collect();
// [(&10, &"a"), (&20, &"b"), (&30, &"c")] ← 40,50 被丢弃

// take / skip: 截取前 n 个 / 跳过前 n 个
let first3: Vec<_> = v.iter().take(3).collect(); // [&10, &20, &30]
let tail: Vec<_> = v.iter().skip(2).collect(); // [&30, &40, &50]
// 天生一对 = 分页: v.iter().skip(offset).take(page_size)

// cloned / copied: 把 &T → T,省去 map(|&x| x) 的模式解引用
let squares: Vec<i32> = v.iter().cloned().map(|x| x * x).collect();
// copied 用于 Copy 类型(i32 等),cloned 用于 Clone 类型(String 等)

常用消费器

消费器驱动整条链,产出最终值.一条完整的迭代器链必须有一个消费器收尾.

// sum / product: 累加或累乘
let total: i32 = v.iter().sum(); // 150
let factorial = (1..=5).product::<i32>(); // 120

// count: 计数,比 .collect::<Vec<_>>().len() 少一次堆分配
let how_many = v.iter().filter(|&&x| x > 25).count(); // 3

// fold: 带初值的聚合."手里拿个篮子,逐个装元素,最后返回篮子"
let sum = v.iter().fold(0, |acc, &x| acc + x); // 求和:150
let prod = v.iter().fold(1, |acc, &x| acc * x); // 求积:120
let max = v.iter().fold(i32::MIN, |acc, &x| acc.max(x)); // 最大值:50

// 复杂例子:用 fold 把数字拼成逗号分隔字符串
let csv = v.iter().fold(String::new(), |mut acc, x| {
if !acc.is_empty() { acc.push(','); }
acc.push_str(&x.to_string());
acc
}); // "10,20,30,40,50"

// for_each: 只做副作用(打印,写文件等),返回 ()
v.iter().for_each(|x| println!("{x}"));

// find: 返回第一个匹配元素,结果是 Option
let first = v.iter().find(|&&x| x > 25); // Some(&30)

// any / all: 存在 / 全称判定
let has_big = v.iter().any(|&&x| x > 100); // false
let all_pos = v.iter().all(|&&x| x > 0); // true

// max / min: 极值,空迭代器返回 None
let biggest = v.iter().max(); // Some(&50)

// nth: 跳过前 n 个取下一个,等价于 .skip(n).next()
let third = v.iter().nth(2); // Some(&30)

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];

// → Vec(最常用)
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();

// → HashMap: 迭代器元素必须是 (K, V) 对
let names = vec!["a", "b", "c"];
let map: HashMap<_, _> = names.iter().enumerate().map(|(i, &n)| (n, i)).collect();
// {"a": 0, "b": 1, "c": 2}

// → HashSet: 去重
let nums = vec![1, 2, 2, 3, 3, 3];
let unique: HashSet<_> = nums.into_iter().collect(); // {1, 2, 3}

// → String: 元素须为 char 或 &str
let chars = vec!['h', 'e', 'l', 'l', 'o'];
let s: String = chars.iter().collect(); // "hello"

类型标注的三种方式

因为 collect 的返回类型是泛型参数,编译器需要知道目标类型.有三种告知方式:

// 方式 1: turbofish(最紧凑)
let v = iter.collect::<Vec<_>>();

// 方式 2: 变量类型标注(最可读,推荐)
let v: Vec<i32> = iter.collect();

// 方式 3: 上下文推断(函数签名,字段类型等)

Result 与 Option 的 collect 魔法

这是 collect 最实用的特殊能力:当迭代器元素是 ResultOption 时,collect 可以"反过来"--把 Vec<Result<T, E>> 收成 Result<Vec<T>, E>,遇到第一个 Err 就短路.

// 场景:解析一批字符串,要么全成功,要么报第一个错
let inputs = vec!["1", "2", "three", "4"];

// 用 collect 的"翻转"能力:
let all_ok: Result<Vec<i32>, _> = inputs.iter()
.map(|s| s.parse::<i32>())
.collect();
// Err(...) ← 到 "three" 就失败,不产出 Vec

// Option 同理:
let nums = vec![Some(1), Some(2), None, Some(4)];
let all_some: Option<Vec<i32>> = nums.into_iter().collect();
// None ← 碰到 None 就整体返回 None

本质是 Rust 为标准库的 ResultOption 分别实现了 FromIterator,逻辑就是"逐元素检查,全成功才产出 Vec,遇失败立即短路".这个模式在解析,校验,批量 IO 场景极常见.

collect 后的所有权

结合前面的知识,collect 产出的集合拥有其元素.迭代时要注意:

let names = vec![String::from("a"), String::from("b")];

// into_iter(): 消耗原集合,元素所有权转移 → collect 产出 Vec<String>
let owned: Vec<String> = names.into_iter().collect();
// names 已死

let names = vec![String::from("a"), String::from("b")];

// iter(): 借用元素,collect 产出 Vec<&String>
let borrowed: Vec<&String> = names.iter().collect();
// names 仍可用

// iter().cloned(): 借用遍历,但 clone 出每个元素 → 产出 Vec<String>
let cloned: Vec<String> = names.iter().cloned().collect();
// names 仍可用,cloned 是拥有自己数据的独立集合

常见组合模式

单个方法威力有限,链式组合才是迭代器的真正优势.下面几种模式在实际代码中出现频率极高.

// 1. 找到某个元素的位置: enumerate + find + map
let pos = v.iter().enumerate()
.find(|(_, &x)| x > 25)
.map(|(i, _)| i); // Some(2),即 30 的索引

// 2. 分页: skip + take
let page = 2; let page_size = 3;
let page_data: Vec<_> = v.iter()
.skip((page - 1) * page_size)
.take(page_size)
.collect(); // [&40, &50]

// 3. 展平嵌套: flatten / flat_map
let nested = vec![vec![1, 2], vec![3, 4, 5]];
let flat: Vec<_> = nested.iter().flatten().collect(); // [&1,&2,&3,&4,&5]
let doubled: Vec<_> = (0..3).flat_map(|x| [x, x * 2]).collect(); // [0,0,1,2,2,4]

// 4. 分组归类: fold + HashMap
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
});
// {'a': ["apple","ant"], 'b': ["banana","bat"]}

// 5. filter_map: 同时过滤和映射,返回 Option 的元素自动滤掉 None
let inputs = vec!["1", "two", "3", "four"];
let nums: Vec<i32> = inputs.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect(); // [1, 3]

最佳实践:优先迭代器链,而非手写索引循环

能用 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 迭代器.

动手练习

  1. 链式求值:用迭代器链求一个向量中所有奇数的平方和.
  2. 惰性验证:写一个只有适配器,没有消费器的迭代器链,观察编译器的 unused 警告,再补上 collect 修复.
  3. enumerate 与 zip:用 enumerate 打印带序号的列表;用 zip 把两个等长向量配对成 Vec<(_, _)>.
  4. fold 与 collect:用 fold 实现求阶乘;用 into_iter().map().collect() 把一个 Vec<String> 全转大写.
  5. 索引循环改写:把一段 for i in 0..v.len() + 索引访问的代码改写成迭代器链,对比两者可读性.