阶段二 · 类型转换与函数式

迭代器与惰性求值

一句话总结

迭代器是 Rust 表达"对一串数据做处理"的核心抽象.
它的两个灵魂特性是惰性(不消费就不计算)和零成本抽象(链式调用编译后和手写循环一样快).理解这两点,就能放心地写出一长串 .map().filter().collect().

前置回顾

前面的字符串三篇中已经大量用到了迭代器:chars(),split(),char_indices() 都返回迭代器.本篇系统讲解迭代器的核心设计,为下一篇闭包打好基础--闭包正是迭代器适配器(如 map,filter)的主要参数.

一切的核心:Iterator 特征

Rust 的迭代器不是某个具体类型,而是任何实现了 Iterator 特征的类型.这个特征的核心只有一个方法:

pub trait Iterator {
type Item; // 关联类型:每次产出什么
fn next(&mut self) -> Option<Self::Item>; // 取下一个,没有了返回 None
}

每调用一次 next(),要么返回 Some(值),要么在耗尽时返回 None.for 循环本质上就是反复调用 next() 直到拿到 None 的语法糖.

迭代器像一台自动售货机:按一次按钮(next()),掉出一瓶饮料(Some);机器空了,再按就什么都不掉(None).
它一次只吐一个,从不提前把所有货都搬出来.

三种迭代:iter / iter_mut / into_iter

从一个集合得到迭代器有三种方式,差别在于拿到的是只读引用,可变引用,还是值本身--这又是所有权在迭代器上的体现.

方法 产出 Item 对原集合的影响
iter() &T 只读借用,原集合保留
iter_mut() &mut T 可变借用,可原地修改元素
into_iter() T 消耗集合,转移元素所有权
let v = vec![1, 2, 3];

let sum: i32 = v.iter().sum(); // 借用,v 之后仍可用
println!("{v:?}"); // [1, 2, 3] 仍然有效

let mut v2 = vec![1, 2, 3];
v2.iter_mut().for_each(|x| *x *= 10); // 原地修改
println!("{v2:?}"); // [10, 20, 30]

let owned: Vec<String> = vec!["a".into(), "b".into()];
for s in owned.into_iter() { } // 取走所有权,之后 owned 不可用

for 循环默认用哪个

for x in &v 等价于 v.iter();for x in &mut v 等价于 v.iter_mut();for x in v 等价于 v.into_iter(),会消耗掉 v.写 for 循环时是否加 &,决定了循环后还能不能继续用这个集合.

适配器 vs 消费者:谁触发计算

迭代器的方法分两大类,这是理解"惰性"的关键.

适配器 (adapter)                消费者 (consumer)
┌──────────────────────────┐ ┌─────────────────┐
│ map / filter / take / │ │ collect / sum / │
│ skip / enumerate / zip │ ───► │ count / fold / │
│ (返回新迭代器,不计算) │ │ for_each / find │
└──────────────────────────┘ │ (触发实际计算) │
惰性,即时返回 └─────────────────┘
拉动整条链条运转
  • 适配器(adapter):如 map,filter,它们接收一个迭代器,返回一个新迭代器,本身不做任何计算,只是"记下要做什么".
  • 消费者(consumer):如 collect,sum,for_each,它们真正开始反复调用 next(),驱动整条链条运转并产出最终结果.

惰性:不消费就不计算

这段代码是经典的迭代器陷阱:

let v = vec![1, 2, 3];

v.iter().map(|x| {
println!("处理 {x}");
x * 2
});
// 什么也不会打印!没有消费者,map 一次都没执行

因为只有适配器,没有消费者,整条链子从未被"拉动",闭包一次都没运行.加上一个消费者,计算才真正发生:

let doubled: Vec<i32> = v.iter().map(|x| {
println!("处理 {x}");
x * 2
}).collect(); // collect 是消费者,此时才打印并计算
// 输出:处理 1 / 处理 2 / 处理 3

编译器会警告未使用的迭代器

对于纯适配器链而不接消费者,编译器通常会给出 unused Map that must be used 的警告.看到这个提示,说明漏掉了 collect,for_each 之类的消费者,链条根本没跑.

惰性带来的好处:短路与无限序列

惰性不只是性能细节,它让一些写法成为可能.find 找到第一个匹配就停止,不会遍历完整集合;(0..).map(...).take(5) 能在一个无限范围上工作,因为 take(5) 只拉动 5 次就停手.若是即时求值,无限序列会直接卡死.

零成本抽象:链式调用不慢

来自其他语言的开发者常担心,map().filter().sum() 这种链式写法,是不是每一步都生成一个中间集合,带来额外开销.在 Rust 里答案是不会.

编译器会把整条迭代器链内联并融合成一个单一循环.下面两段代码编译后的机器码基本一致:

// 迭代器风格
let sum: i32 = (1..=100)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();

// 等价的手写循环
let mut sum = 0;
for x in 1..=100 {
if x % 2 == 0 {
sum += x * x;
}
}

零成本抽象就像点一份"套餐":菜单上分了三道工序(filter, map, sum),但后厨实际上一锅出,并不会真的摆三张桌子来回搬运.享受了清晰的表达,却没付中间数据的钱.

零成本的准确含义

Rust 的零成本抽象(zero-cost abstraction)指:不用的特性不付代价,用的特性手写也快不过它.迭代器链经过单态化(monomorphization)和内联,编译产物与手写循环等价.所以选择迭代器风格几乎纯粹是可读性的考量,而非性能取舍--这点和很多语言相反.

常用方法速查

方法 类别 作用
map(f) 适配器 对每个元素应用 f
filter(pred) 适配器 保留满足条件的元素
enumerate() 适配器 产出 (索引, 元素)
zip(other) 适配器 把两个迭代器配对
take(n) / skip(n) 适配器 取前 n 个 / 跳过前 n 个
collect() 消费者 收集进集合(需类型标注)
sum() / count() 消费者 求和 / 计数
fold(init, f) 消费者 带初值的累积归约
find(pred) 消费者 返回首个匹配(短路)

collect 靠类型标注决定收进什么

collect() 极其灵活,能收进 Vec,HashMap,String 甚至 Result.但它必须知道目标类型,所以常要写 let v: Vec<_> = ...collect(); 或用 turbofish collect::<Vec<_>>().

一个强大用法是把 Iterator<Item=Result<T, E>> 收成 Result<Vec<T>, E>--任一元素出错则整体为 Err.

实现自己的迭代器

为自定义类型实现 Iterator,只需提供 next(),其余几十个适配器和消费者方法全部免费获得.

struct Counter { count: u32 }

impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 3 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}

let total: u32 = Counter { count: 0 }
.map(|x| x * 10)
.sum(); // 60,map/sum 直接可用,无需额外实现

快速回顾

  • Iterator 核心是 next(),for 循环是反复调用它的语法糖
  • 三种迭代:iter() 借用,iter_mut() 可变借用,into_iter() 取所有权
  • 适配器惰性,消费者触发计算,只写适配器不接消费者等于什么都没做
  • 零成本抽象:链式调用编译后与手写循环等价,选它是为可读性
  • 自定义迭代器只需实现 next(),即可白嫖全部适配器与消费者
  • 下一篇讲闭包--迭代器适配器里那些 |x| ... 到底是什么,以及 Fn/FnMut/FnOnce 的区分

动手练习

  1. 观察惰性:写一段只有适配器,没有消费者的 map 链,观察编译器警告;再补上 collect,确认闭包此时才执行.
  2. 一行链式:用迭代器一行实现:从 1..=20 中取出所有偶数的平方,收集成 Vec<i32>.
  3. 自定义迭代器:实现一个产出斐波那契数列的自定义迭代器,配合 take(10) 打印前十项,体会无限序列 + 惰性的组合.