一、为什么需要迭代器
你要遍历一个数组,用 for 循环加下标。换成链表,得改成 while 加指针。换成二叉树,又得写递归。每种数据结构都有自己的遍历方式,消费者必须了解容器的内部结构才能遍历它。这有两个问题:一是代码无法通用,换个容器就要改遍历逻辑;二是暴露了内部实现细节,违反了封装原则。
来看一段具体的代码。假设你要对三种不同的数据结构执行同一个操作——打印所有元素:
// 数组:下标遍历int[] arr = {1, 2, 3};for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]);}
// 链表:指针遍历ListNode node = head;while (node != null) { System.out.println(node.val); node = node.next;}
// 二叉树:递归遍历void inorder(TreeNode root) { if (root == null) return; inorder(root.left); System.out.println(root.val); inorder(root.right);}三种数据结构,三种完全不同的遍历方式。如果业务逻辑从「打印」变成「过滤再打印」,你得改三处。如果新增一种数据结构,又得写一套新的遍历代码。迭代器要解决的就是这个问题——用统一的 next() 接口屏蔽遍历差异:
// 不管底层是数组、链表还是树,调用方式完全一样Iterator<Integer> it = container.iterator();while (it.hasNext()) { System.out.println(it.next());}消费者不再关心容器内部是数组还是树,只要拿到迭代器,就能逐个取出元素。
更实际的问题是内存。你需要从日志文件中找出前 10 条错误记录。文件有几百万行,你不可能全部加载到内存再过滤。如果用急切求值(Eager Evaluation)的方式,先把所有行读进一个列表,再用 filter 筛选,内存峰值就是整个文件的大小——几 GB 的数据瞬间撑爆堆。
更隐蔽的问题在于中间结果的累积。假设你要对数据做三步转换:先过滤,再映射,最后取前 N 个。急切求值会在每一步都生成一个完整的中间集合:
# 急切求值:每一步都生成完整中间列表lines = open("huge.log").readlines() # 列表 1:全部行errors = [l for l in lines if "ERROR" in l] # 列表 2:错误行messages = [l.strip() for l in errors] # 列表 3:清洗后的行top10 = messages[:10] # 列表 4:最终结果四份列表同时存在于内存中,而你最终只需要 10 条记录。这就是急切求值的代价——中间结果全部物化,即使大部分数据会被下一步丢弃。
迭代器把这种「逐个产出、惰性求值」的行为抽象成统一的接口——next() 方法。转换操作(map、filter、take)被惰性链式调用,直到终端操作(collect、for-each)驱动整个链执行。数据像水流过管道,每次只有一个元素在管道中,没有中间集合,没有内存浪费。
二、现实类比
一叠面朝下的扑克牌。你每次翻一张,不知道下面是什么。不需要把整副牌摊开——只要不断翻,直到结束或者你决定停下。
这个类比还揭示了迭代器的另一个特性:一次性。翻过的牌不会再翻回来,你想重新看一遍?拿一副新牌从头翻。
三、核心思想
迭代器通过 next() 方法逐个产出值。转换操作(map、filter、fold)惰性链式调用——直到终端操作驱动整个链。这意味着中间过程零分配,每次只有一个元素流经管道。
3.1 惰性链式调用的工作原理
理解惰性链的关键在于区分两类操作:
- 中间操作(Intermediate Operation):
map、filter、take、skip等。它们不执行任何计算,只是把转换逻辑注册到管道上,返回一个新的迭代器。 - 终端操作(Terminal Operation):
collect、fold、count、for_each等。它们触发整个管道的执行,真正开始从源头拉取数据。
中间操作就像搭管道——你把一节一节的管子接起来,但水还没有流。终端操作就像打开水龙头——水从源头流出,经过每一节管子,最终到达终点。
来看一个完整的执行流程。假设有 10 个元素 [1, 2, ..., 10],我们要取前 3 个奇数再乘以 10:
执行过程是拉取驱动的:终端操作 collect 向 take(3) 要元素,take(3) 向 map(x10) 要元素,map(x10) 向 filter(isOdd) 要元素,filter(isOdd) 向源头要元素。每一层只在被下游拉取时才向上游拉取,形成一条从终端到源头的调用链。
具体步骤如下:
collect向take(3)请求第 1 个元素take(3)计数器为 0,向map(x10)请求元素map(x10)向filter(isOdd)请求元素filter(isOdd)从源取 1,是奇数,产出 1map(x10)收到 1,产出 10take(3)收到 10,计数器变 1,产出 10collect收到 10- …… 重复上述过程,直到
take(3)产出 3 个元素后停止
整个管道只处理到第 5 个元素就停了。take(3) 从 filter 拉取,filter 从源拉取,每次一个。这就是惰性求值的威力——按需驱动,没有浪费。
| 属性 | 值 |
|---|---|
next() | O(1) 每元素——管道中的一步 |
| 内存 | O(1)——无中间集合,一次一个元素 |
| 短路 | take(k) 提前停止——只处理 k 个元素 |
| 可组合性 | 链式 map/filter/fold 无需分配中间数组 |
3.2 迭代器的一次性
迭代器是一次性的游标,不是数据的容器。调用 next() 推进游标,消费完毕后 next() 永远返回终止信号。这和集合有本质区别:集合是数据本身,可以反复遍历;迭代器是遍历过程,过程不可逆。
nums = iter([1, 2, 3])list(nums) # [1, 2, 3]list(nums) # [] —— 已经消费完了想再遍历一次?创建新的迭代器,或者先 collect 成集合。这个设计不是缺陷,而是刻意为之——一次性意味着迭代器不需要保存已消费的数据,内存开销始终是 O(1)。如果迭代器可以反复遍历,它就必须缓存所有历史数据,那就退化成了集合。
四、变体与对比
| 模式 | 与迭代器的关系 | 关键区别 |
|---|---|---|
| 合并迭代器 | 将多个有序迭代器合并为一个有序输出 | 多源归一,要求各源有序 |
| 访问者 | 两者都遍历数据结构 | 迭代器产出元素,访问者分发回调;迭代器不感知元素类型,访问者按类型分派 |
| 中间件/管道链 | 中间件链遍历处理器序列 | 中间件是推送模型,数据主动流过;迭代器是拉取模型,消费者主动取 |
| 观察者 | 互为对偶 | 迭代器是拉取模型(消费者主动取),观察者是推送模型(生产者主动推) |
| 生成器函数 | 迭代器的一种实现方式 | 用 yield 语法糖简化迭代器的编写,编译器自动生成状态机 |
4.1 生成器函数:迭代器的语法糖
手写迭代器需要维护游标状态、判断终止条件、处理边界情况,代码往往冗长且容易出错。生成器函数用 yield 关键字简化了这一过程——你在函数里写循环,编译器自动把函数转换成状态机,每次 yield 暂停执行,下次调用从暂停处恢复。
# 手写迭代器:需要自己管理状态class RangeIter: def __init__(self, start, end): self.current = start self.end = end
def __iter__(self): return self
def __next__(self): if self.current >= self.end: raise StopIteration val = self.current self.current += 1 return val
# 生成器函数:编译器自动管理状态def range_gen(start, end): current = start while current < end: yield current current += 1生成器本质上就是迭代器——它实现了 next() 接口,支持 for...of / for...in 循环。区别只在于编写方式:生成器用命令式的代码描述产出逻辑,编译器负责转换成状态机;手写迭代器则需要你自己实现状态机。
4.2 迭代器 vs 集合:惰性与急切
迭代器和集合代表两种不同的求值策略:
| 维度 | 迭代器(惰性) | 集合(急切) |
|---|---|---|
| 求值时机 | 终端操作触发 | 创建时立即求值 |
| 内存 | O(1),不存储数据 | O(n),存储全部数据 |
| 遍历次数 | 一次性 | 可反复遍历 |
| 短路能力 | 支持 take、find 等 | 不支持,必须先计算全部 |
| 随机访问 | 不支持 | 支持 |
| 适用场景 | 大数据、无限序列、流式处理 | 小数据、需要多次遍历、需要索引访问 |
选择原则很简单:数据量大或只需遍历一次,用迭代器;数据量小或需要多次访问,用集合。很多语言同时提供两者,让你按需选择。Rust 的 Iterator 和 Vec、Java 的 Stream 和 List、Python 的生成器和列表,都是这个思路。
五、多语言实现
5.1 Go 实现(Go 1.23+ iter.Seq)
package iterator
import "iter"
// Filter 返回惰性过滤后的序列func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] { return func(yield func(T) bool) { for v := range seq { if pred(v) && !yield(v) { return } } }}
// Map 返回惰性映射后的序列func Map[T, U any](seq iter.Seq[T], fn func(T) U) iter.Seq[U] { return func(yield func(U) bool) { for v := range seq { if !yield(fn(v)) { return } } }}
// Take 限制最多产出 n 个元素func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] { return func(yield func(T) bool) { i := 0 for v := range seq { if i >= n || !yield(v) { return } i++ } }}
// Collect 将惰性序列物化为切片func Collect[T any](seq iter.Seq[T]) []T { var out []T for v := range seq { out = append(out, v) } return out}Go 1.23 引入的 iter.Seq 让惰性迭代成为一等公民。它的设计很巧妙:iter.Seq[T] 的类型签名是 func(yield func(T) bool),本质上是一个接受回调函数的高阶函数。yield 参数由 range 机制注入——当消费者用 for v := range seq 遍历时,Go 自动提供一个 yield 函数,每次调用 yield(v) 就把 v 交给消费者。
yield 返回 bool,这个设计解决了提前终止的问题。当消费者 break 退出 range 循环时,yield 返回 false,生产者检查到这个信号后立即 return,停止产出。这比传统的 Close 方法更优雅——不需要额外的接口,不需要 defer 清理,一个布尔返回值就搞定了。
整个链式调用的惰性机制也由此实现:Filter 返回的新 iter.Seq 内部 range 遍历上游,只在 pred(v) 为真时才调用 yield(v)。如果下游不再消费(yield 返回 false),上游的 range 也会停止。信号从终端操作逐层向上传播,和 Rust、Java Stream 的拉取模型殊途同归。
5.2 TypeScript 实现
class Iter<T> { constructor(private source: () => Generator<T>) {}
static from<T>(items: T[]): Iter<T> { return new Iter(function* () { yield* items; }); }
// 惰性映射:不执行,只记录转换逻辑 map<U>(fn: (x: T) => U): Iter<U> { const source = this.source; return new Iter(function* () { for (const item of source()) yield fn(item); }); }
// 惰性过滤 filter(pred: (x: T) => boolean): Iter<T> { const source = this.source; return new Iter(function* () { for (const item of source()) if (pred(item)) yield item; }); }
// 短路:取前 n 个 take(n: number): Iter<T> { const source = this.source; return new Iter(function* () { let i = 0; for (const item of source()) { if (i++ >= n) break; yield item; } }); }
// 终端操作:物化为数组 collect(): T[] { return [...this.source()]; }}TypeScript 用 Generator 实现惰性链。map 和 filter 只是注册转换逻辑,不做任何计算。直到 collect() 被调用,整个管道才被驱动执行。
这里有一个设计细节值得注意:source 的类型是 () => Generator<T> 而不是 Generator<T>。这是因为 Generator 是一次性的——如果直接持有 Generator 实例,collect() 调用一次后迭代器就耗尽了。把 source 存为工厂函数,每次调用 source() 都创建一个新的 Generator,就能支持多次 collect()。当然,如果底层源本身就是一次性的(比如文件流),多次调用工厂函数也不会得到新数据——这和迭代器一次性本质是一致的。
Generator 的 yield 暂停/恢复机制由 JavaScript 引擎实现,本质上是一个状态机。每次调用 next() 恢复执行,遇到 yield 再次暂停。for...of 循环就是不断调用 next() 直到 { done: true } 的语法糖。break 退出循环时,引擎会自动调用 Generator 的 return() 方法,执行清理逻辑。
六、生产验证
6.1 Rust:next() 是唯一的基石
Rust 标准库的 Iterator trait 只要求实现一个方法:next()。map、filter、fold、collect、take、zip、enumerate……几十个方法全部构建在 next() 之上。
这个设计哲学叫做最小接口原则:只要你能逐个产出元素,你就是迭代器。至于怎么组合、怎么转换,那是组合子的职责,不需要每个迭代器单独实现。
// 自定义迭代器只需实现 next()struct Counter { count: u32, max: u32,}
impl Iterator for Counter { type Item = u32;
fn next(&mut self) -> Option<Self::Item> { if self.count < self.max { self.count += 1; Some(self.count) } else { None } }}
// 自动获得 map、filter、collect 等几十个方法let sum: u32 = Counter { count: 0, max: 5 } .map(|x| x * 2) .filter(|x| x > 3) .sum();// 等于 6 + 8 + 10 = 24Rust 的零成本抽象在这里体现得淋漓尽致:map 返回的 Map<I, F> 是一个编译期确定的类型,没有虚函数调用,没有堆分配。整个迭代器链在编译后等价于手写的循环——你付出了抽象的表达力,但没有付出运行时的代价。
6.2 Python:生成器的底层实现
Python 的生成器是迭代器最广泛的使用形态。genobject.c 中的 gen_send_ex2 函数是核心:它把参数推送到帧栈,调用 _PyEval_EvalFrame 执行字节码,遇到 YIELD_VALUE 指令时暂停,返回产出的值;遇到 RETURN_VALUE 指令时终止,抛出 StopIteration。
生成器在 Python 中的地位如此重要,以至于标准库里大量 API 都返回生成器而非列表:range()、map()、filter()、zip()、enumerate()、reversed()、文件对象的迭代……Python 3 把 range() 从返回列表改为返回惰性序列,就是这个设计哲学的体现。
# Python 2:range(1000000000) 立即分配 8GB 内存# Python 3:range(1000000000) 返回惰性序列,几乎零内存r = range(1_000_000_000)first_three = list(r[:3]) # 只计算 3 个值6.3 Java Streams:显式的惰性管线
Java 8 引入的 Stream API 把惰性管线做成了显式的语言特性。中间操作(filter、map、flatMap)返回新的 Stream,不触发计算;终端操作(collect、forEach、reduce)触发整个管道执行。这种设计让「哪些操作是惰性的」一目了然——返回 Stream 的就是惰性的,返回其他类型的就是终端的。
Java Stream 还有一个独特的设计:流只能消费一次。调用终端操作后,Stream 就失效了,再次使用会抛出 IllegalStateException。这和迭代器的一次性本质一致,但 Java 选择了运行时检查而非编译时保证(Rust 的所有权系统在编译时就能防止重复消费)。
七、小结
何时使用:
- 大规模或无限序列——处理百万行数据无需全部加载到内存,
range(1_000_000_000)在 Python 3 中几乎零内存 - 可组合管线——链式转换无中间分配,代码简洁且高效
- 提前终止——在十亿元素源上
take(5)只处理 5 个 - 流处理——文件、网络数据、事件流,数据逐个到达
何时不用:
- 随机访问——迭代器是顺序的,需要按索引访问时用数组
- 多次遍历——大多数迭代器是一次性的,需要反复遍历时先 collect 成集合
- 简单循环——普通
for循环比单步迭代器链更清晰时,别过度抽象
需要警惕的陷阱:
-
无限迭代器 + collect:对无限迭代器调用
collect()会导致死循环或内存耗尽。take()是你的安全阀——先限制数量,再物化。# 危险:永远不会结束list(itertools.count())# 安全:先 take 再 collectlist(itertools.islice(itertools.count(), 10)) -
迭代器耗尽后误用:迭代器消费完毕后
next()返回终止信号,但不会报错(Python 除外,会抛StopIteration)。如果你忘了检查返回值,可能静默地跳过逻辑。 -
带副作用的中间操作:在
map里做 I/O 或修改外部状态是危险的——惰性求值意味着你无法确定操作何时执行,甚至是否执行。终端操作之前的中间操作可能根本不会运行。
八、参考资料
- Rust Iterator trait 文档 - 零成本抽象迭代器的设计哲学
- Go 1.23 iter 包文档 - Go 官方迭代器支持
- Python 生成器 PEP 255 - Python 生成器的原始提案
- Java Streams 文档 - 惰性管线的设计与实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






