一句话总结
迭代器是 Rust 表达"对一串数据做处理"的核心抽象.
它的两个灵魂特性是惰性(不消费就不计算)和零成本抽象(链式调用编译后和手写循环一样快).理解这两点,就能放心地写出一长串 .map().filter().collect().
前置回顾
前面的字符串三篇中已经大量用到了迭代器:chars(),split(),char_indices() 都返回迭代器.本篇系统讲解迭代器的核心设计,为下一篇闭包打好基础--闭包正是迭代器适配器(如 map,filter)的主要参数.
一切的核心:Iterator 特征
Rust 的迭代器不是某个具体类型,而是任何实现了 Iterator 特征的类型.这个特征的核心只有一个方法:
|
每调用一次 next(),要么返回 Some(值),要么在耗尽时返回 None.for 循环本质上就是反复调用 next() 直到拿到 None 的语法糖.
迭代器像一台自动售货机:按一次按钮(next()),掉出一瓶饮料(Some);机器空了,再按就什么都不掉(None).
它一次只吐一个,从不提前把所有货都搬出来.
三种迭代:iter / iter_mut / into_iter
从一个集合得到迭代器有三种方式,差别在于拿到的是只读引用,可变引用,还是值本身--这又是所有权在迭代器上的体现.
| 方法 | 产出 Item | 对原集合的影响 |
|---|---|---|
iter() |
&T |
只读借用,原集合保留 |
iter_mut() |
&mut T |
可变借用,可原地修改元素 |
into_iter() |
T |
消耗集合,转移元素所有权 |
|
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):如
map,filter,它们接收一个迭代器,返回一个新迭代器,本身不做任何计算,只是"记下要做什么". - 消费者(consumer):如
collect,sum,for_each,它们真正开始反复调用next(),驱动整条链条运转并产出最终结果.
惰性:不消费就不计算
这段代码是经典的迭代器陷阱:
|
因为只有适配器,没有消费者,整条链子从未被"拉动",闭包一次都没运行.加上一个消费者,计算才真正发生:
|
编译器会警告未使用的迭代器
对于纯适配器链而不接消费者,编译器通常会给出 unused Map that must be used 的警告.看到这个提示,说明漏掉了 collect,for_each 之类的消费者,链条根本没跑.
惰性带来的好处:短路与无限序列
惰性不只是性能细节,它让一些写法成为可能.find 找到第一个匹配就停止,不会遍历完整集合;(0..).map(...).take(5) 能在一个无限范围上工作,因为 take(5) 只拉动 5 次就停手.若是即时求值,无限序列会直接卡死.
零成本抽象:链式调用不慢
来自其他语言的开发者常担心,map().filter().sum() 这种链式写法,是不是每一步都生成一个中间集合,带来额外开销.在 Rust 里答案是不会.
编译器会把整条迭代器链内联并融合成一个单一循环.下面两段代码编译后的机器码基本一致:
|
零成本抽象就像点一份"套餐":菜单上分了三道工序(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(),其余几十个适配器和消费者方法全部免费获得.
|
快速回顾
- Iterator 核心是
next(),for循环是反复调用它的语法糖 - 三种迭代:
iter()借用,iter_mut()可变借用,into_iter()取所有权 - 适配器惰性,消费者触发计算,只写适配器不接消费者等于什么都没做
- 零成本抽象:链式调用编译后与手写循环等价,选它是为可读性
- 自定义迭代器只需实现
next(),即可白嫖全部适配器与消费者 - 下一篇讲闭包--迭代器适配器里那些
|x| ...到底是什么,以及Fn/FnMut/FnOnce的区分
动手练习
- 观察惰性:写一段只有适配器,没有消费者的
map链,观察编译器警告;再补上collect,确认闭包此时才执行. - 一行链式:用迭代器一行实现:从
1..=20中取出所有偶数的平方,收集成Vec<i32>. - 自定义迭代器:实现一个产出斐波那契数列的自定义迭代器,配合
take(10)打印前十项,体会无限序列 + 惰性的组合.