Ruby 中的 Enumerator 对象实际上是如何获取值的?

How are values actually fetched from an Enumerator object in Ruby?

我对如何从 Enumerator 对象中获取值很感兴趣。在下面的代码中,我期待第一个 enum.next 调用引发异常,因为在调用 enum.to_a.

之后已经从 enum 收到了所有值
enum = Enumerator.new do |yielder|
  yielder.yield 1
  yielder.yield 2
  yielder.yield 3
end

p enum.to_a # => [1, 2, 3]

puts enum.next # Expected StopIteration here
puts enum.next
puts enum.next
puts enum.next # => StopIteration exception raised

Enumerator 的实例上调用 next 与像 to_a 这样的迭代器方法有什么区别?

调用#next将内部位置向前移动,而#to_a根本不考虑内部位置。尝试调用 next 一次,然后调用 to_a,然后再次调用 next 进行实验。

https://ruby-doc.org/core-2.4.0/Enumerator.html#method-i-next

https://ruby-doc.org/core-2.4.0/Enumerable.html#method-i-to_a

简短回答:to_a 总是遍历所有元素并且不推进迭代器的位置。这就是为什么即使您之前调用过 to_aEnumerator#next 也会从第一个元素开始。调用 to_a 不会修改枚举器对象。


详情如下:

术语:内部迭代与外部迭代

在 Ruby 中讨论迭代器时,会出现两个术语:

  1. internal iteration(也称为隐式迭代)
  2. external iteration

在您的问题中,enum.to_aenum 用于内部迭代的示例,而 enum.next 是外部迭代的示例。

外部迭代提供了更多的控制,但它是一种更底层的操作。内部迭代通常更优雅。不同之处在于外部迭代使状态显式(当前位置),而内部迭代隐式应用于所有元素。

内部迭代:to_a

to_a 将调用 Enumerator#each,它根据此 Enumerator 的构造方式.

遍历块

这是关键点。由于它不对调用它的枚举器对象的内部状态(位置)进行操作, 它不会干扰对 next 的调用(外部迭代操作)。

外部迭代:下一个

创建 Enumerator 对象时,其状态被初始化为指向第一个对象。您可以通过调用 next 来修改内部状态,这将推进位置。一旦所有元素都被消耗,它将引发 StopIteration 异常。

请注意,仅当您使用枚举器对象进行外部迭代时,状态才相关。这就解释了为什么你可以安全地调用 to_a 一个已经消耗了所有元素的枚举器,它仍然会 return 一个包含所有元素的列表。所有内部迭代操作(例如,eachto_a, map`)都不会干扰外部迭代。

在 Rubinius 中的实现

我查看了 Rubinius 源代码以了解它是如何在那里实现的。虽然不是语言规范,但应该比较接近真相。入口点:

请注意,Enumerator 包含 Enumerable 作为混合。