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_a
,Enumerator#next 也会从第一个元素开始。调用 to_a
不会修改枚举器对象。
详情如下:
术语:内部迭代与外部迭代
在 Ruby 中讨论迭代器时,会出现两个术语:
- internal iteration(也称为隐式迭代)
- external iteration
在您的问题中,enum.to_a
是 enum
用于内部迭代的示例,而 enum.next
是外部迭代的示例。
外部迭代提供了更多的控制,但它是一种更底层的操作。内部迭代通常更优雅。不同之处在于外部迭代使状态显式(当前位置),而内部迭代隐式应用于所有元素。
内部迭代:to_a
to_a
将调用 Enumerator#each,它根据此 Enumerator 的构造方式.
遍历块
这是关键点。由于它不对调用它的枚举器对象的内部状态(位置)进行操作,
它不会干扰对 next
的调用(外部迭代操作)。
外部迭代:下一个
创建 Enumerator 对象时,其状态被初始化为指向第一个对象。您可以通过调用 next
来修改内部状态,这将推进位置。一旦所有元素都被消耗,它将引发 StopIteration
异常。
请注意,仅当您使用枚举器对象进行外部迭代时,状态才相关。这就解释了为什么你可以安全地调用 to_a
一个已经消耗了所有元素的枚举器,它仍然会 return 一个包含所有元素的列表。所有内部迭代操作(例如,each
、to_a,
map`)都不会干扰外部迭代。
在 Rubinius 中的实现
我查看了 Rubinius 源代码以了解它是如何在那里实现的。虽然不是语言规范,但应该比较接近真相。入口点:
请注意,Enumerator 包含 Enumerable 作为混合。
我对如何从 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_a
,Enumerator#next 也会从第一个元素开始。调用 to_a
不会修改枚举器对象。
详情如下:
术语:内部迭代与外部迭代
在 Ruby 中讨论迭代器时,会出现两个术语:
- internal iteration(也称为隐式迭代)
- external iteration
在您的问题中,enum.to_a
是 enum
用于内部迭代的示例,而 enum.next
是外部迭代的示例。
外部迭代提供了更多的控制,但它是一种更底层的操作。内部迭代通常更优雅。不同之处在于外部迭代使状态显式(当前位置),而内部迭代隐式应用于所有元素。
内部迭代:to_a
to_a
将调用 Enumerator#each,它根据此 Enumerator 的构造方式.
这是关键点。由于它不对调用它的枚举器对象的内部状态(位置)进行操作,
它不会干扰对 next
的调用(外部迭代操作)。
外部迭代:下一个
创建 Enumerator 对象时,其状态被初始化为指向第一个对象。您可以通过调用 next
来修改内部状态,这将推进位置。一旦所有元素都被消耗,它将引发 StopIteration
异常。
请注意,仅当您使用枚举器对象进行外部迭代时,状态才相关。这就解释了为什么你可以安全地调用 to_a
一个已经消耗了所有元素的枚举器,它仍然会 return 一个包含所有元素的列表。所有内部迭代操作(例如,each
、to_a,
map`)都不会干扰外部迭代。
在 Rubinius 中的实现
我查看了 Rubinius 源代码以了解它是如何在那里实现的。虽然不是语言规范,但应该比较接近真相。入口点:
请注意,Enumerator 包含 Enumerable 作为混合。