Skip to main content

迭代器的接口对比

· 6 min read

迭代器的接口需要包含三种基本操作:

  • 判断是否还有元素
  • 获取当前元素
  • 移动到下一个元素

如图所示,迭代器的接口设计主要有下面几个流派:

C++ 的设计

C++ 将这三种操作分别封装成三个函数,分别是 operator!=operator*operator++

for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << std::endl;
}

这一设计最大的好处就是兼容了 C 语言的指针语义,使得指向数组的指针可以直接像迭代器一样使用。同时由于迭代器的三种操作是分开的,操作起来可以更加灵活。

但是问题也很明显,那就是迭代器本身不是自包含的,需要额外提供一个 end() 函数返回的迭代器作为终止条件。在遍历数组或链表时,这个迭代器尚且可以表示 past-the-last 元素,但是对于其他数据结构,这个迭代器可能就没有实际意义了。

C++20 引入了一种称为哨兵迭代器的设计。采用这种设计的迭代器不再需要专门准备一个终止迭代器,而是使用通用的无状态的哨兵迭代器。在与哨兵迭代器比较时,这类迭代器直接通过本身的状态来判断是否还有元素,使得迭代器实现了自包含。

Java 的设计

Java 将这三种操作封装成两个函数,分别是 hasNextnextnext 在返回当前元素的同时,还会移动到下一个元素。当然还有一种理解,认为 Java 的迭代器指向两个元素之间的空隙,next 的时候移动到下一个空隙的同时返回越过的元素。

Iterator<Integer> it = v.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

Java 的设计需要迭代器有预测性,即我们希望在实际调用 next 之前就能通过 hasNext 知道是否还有元素。对于基于容器的迭代器来说,这可能比较容易实现。但是对于其他场景(如生成器协程),这种设计就不太适用了。因此在一些 Java 的迭代器实现中,hasNext 就已经计算出了下一个元素并缓存在了迭代器内部,next 直接返回这个缓存的元素。这会导致看似开销较小的 hasNext 操作实际上是一个隐含的 next 操作,而 next 操作并没有执行真正的计算。同时还需要注意,这里的 hasNext 应该被设计为幂等的操作,即多次调用 hasNext 应该返回相同的结果,且不影响 next 的结果。

C# 的设计

C# 将这三种操作封装成两个函数,分别是 MoveNextCurrentMoveNext 在判断是否还有元素的同时,还会移动到下一个元素。

IEnumerator<int> it = v.GetEnumerator();
while (it.MoveNext()) {
Console.WriteLine(it.Current);
}

与 Java 相比,C# 的设计是反其道而行之。幂等的操作变成了 Current,这导致所有的计算都应该发生在 MoveNext 之中,Current 只应该是一个访问操作。可以是访问容器,也可以是访问迭代器中的缓存。当然,如果你执意要在 Current 计算,也不是不可以。

Rust 和 JavaScript 的设计

Rust 将这三种操作封装成一个函数,即 nextnext 在返回当前元素的同时,还会移动到下一个元素。根据返回的 Option 类型来判断是否还有元素,有则返回 Some,无则返回 None

let mut it = v.iter();
while let Some(x) = it.next() {
println!("{}", x);
}

JavaScript 的迭代器设计与 Rust 类似,也是将这三种操作封装成一个函数,即 nextnext 在返回当前元素的同时,还会移动到下一个元素。根据返回的 done 字段来判断是否还有元素。

let it = v[Symbol.iterator]();
let next = it.next();
while (!next.done) {
console.log(next.value);
next = it.next();
}

这两个语言直接合并了三个操作,彻底抛弃了在迭代器中可能存在的缓存。返回的类型被套上了一层壳,应该先判断存在与否,再进行使用。

Python 的设计

Python __next__ 在返回当前元素的同时,还会移动到下一个元素。当元素遍历完毕时,会抛出 StopIteration 异常。

it = iter(v)
while True:
try:
print(next(it))
except StopIteration:
break

这种方式虽然比较低效,但实际上也一点都不快。