mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3727 字
10 分钟
迭代器(Iterator)
2026-06-13

一、为什么需要迭代器#

你要遍历一个数组,用 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):mapfiltertakeskip 等。它们不执行任何计算,只是把转换逻辑注册到管道上,返回一个新的迭代器。
  • 终端操作(Terminal Operation):collectfoldcountfor_each 等。它们触发整个管道的执行,真正开始从源头拉取数据。

中间操作就像搭管道——你把一节一节的管子接起来,但水还没有流。终端操作就像打开水龙头——水从源头流出,经过每一节管子,最终到达终点。

来看一个完整的执行流程。假设有 10 个元素 [1, 2, ..., 10],我们要取前 3 个奇数再乘以 10:

flowchart LR S["源\n[1,2,...,10]"] --> F["filter\nisOdd"] --> M["map\nx10"] --> T["take 3"] --> C["collect\n[10,30,50]"]

执行过程是拉取驱动的:终端操作 collecttake(3) 要元素,take(3)map(x10) 要元素,map(x10)filter(isOdd) 要元素,filter(isOdd) 向源头要元素。每一层只在被下游拉取时才向上游拉取,形成一条从终端到源头的调用链。

具体步骤如下:

  1. collecttake(3) 请求第 1 个元素
  2. take(3) 计数器为 0,向 map(x10) 请求元素
  3. map(x10)filter(isOdd) 请求元素
  4. filter(isOdd) 从源取 1,是奇数,产出 1
  5. map(x10) 收到 1,产出 10
  6. take(3) 收到 10,计数器变 1,产出 10
  7. collect 收到 10
  8. …… 重复上述过程,直到 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),存储全部数据
遍历次数一次性可反复遍历
短路能力支持 takefind不支持,必须先计算全部
随机访问不支持支持
适用场景大数据、无限序列、流式处理小数据、需要多次遍历、需要索引访问

选择原则很简单:数据量大或只需遍历一次,用迭代器;数据量小或需要多次访问,用集合。很多语言同时提供两者,让你按需选择。Rust 的 IteratorVec、Java 的 StreamList、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 实现惰性链。mapfilter 只是注册转换逻辑,不做任何计算。直到 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()mapfilterfoldcollecttakezipenumerate……几十个方法全部构建在 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 = 24

Rust 的零成本抽象在这里体现得淋漓尽致: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 把惰性管线做成了显式的语言特性。中间操作(filtermapflatMap)返回新的 Stream,不触发计算;终端操作(collectforEachreduce)触发整个管道执行。这种设计让「哪些操作是惰性的」一目了然——返回 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 再 collect
    list(itertools.islice(itertools.count(), 10))
  • 迭代器耗尽后误用:迭代器消费完毕后 next() 返回终止信号,但不会报错(Python 除外,会抛 StopIteration)。如果你忘了检查返回值,可能静默地跳过逻辑。

  • 带副作用的中间操作:在 map 里做 I/O 或修改外部状态是危险的——惰性求值意味着你无法确定操作何时执行,甚至是否执行。终端操作之前的中间操作可能根本不会运行。

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

迭代器(Iterator)
https://blog.souloss.com/posts/programming/behavioral/behavioral-iterator/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时