枚举器如何在方法执行过程中停止?

How can Enumerator stop in the middle of method execution?

(示例来自 Ruby Tapas 剧集。59)

@names = %w[Ylva Brighid Shifra Yesamin]

def names
  yield @names.shift
  yield @names.shift
  yield @names.shift
  yield @names.shift
end

enum = to_enum(:names)
enum.next # => Ylva
@names    # => ["Brighid", "Shifra", "Yesamin"]

names 方法执行似乎在第一行之后停止。如果 names 完全执行,@names 应该变为空。这种魔法(=部分调用方法)是如何发生的?

定义

Object#to_enum 的文档(与 Object#enum_for 相同)解释说,在没有块的情况下调用时:

obj.to_enum(method = :each, *args)

该方法“创建一个新的枚举器,它将通过在 obj 上调用 method 进行枚举,传递 args,如果有的话。由于此方法是在 Object 上创建的它可以在任何对象上调用,但是如果 each 没有在对象的 class:

上定义,那么这样做是没有意义的
enum = 1.to_enum
enum.each { |i| puts "i" }
  #NoMethodError: undefined method `each' for 1:Fixnum

正常使用

通常会看到 to_enum 与默认方法参数 :each 一起使用,没有参数和显式接收者:

obj.to_enum

我敢说 obj 通常是一个数组。在你的问题中,方法参数不是 :each 并且接收者是隐式的,因此 self,等于 main.

工作原理

一旦枚举数 enum 被定义,如果 each 被块调用,enum 的每个元素被传递给块(并分配给块变量) 并计算块。

以下操作序列应该更清楚地说明枚举器的工作原理:

a = [1,2,3]
enum = a.to_enum
  #=> #<Enumerator: [1, 2, 3]:each> 
enum.to_a
  #=> [1, 2, 3] 
enum.each { |e| puts e }
  #-> 1
  #   2
  #   3
  #=> [1, 2, 3] 
a[0] = 'cat'
enum.to_a
  #=> ["cat", 2, 3] 
a.object_id
  #=> 70235487149000
a = []
a.object_id
  #=> 70235487117180
enum.to_a
  #=> ["cat", 2, 3] !!
a = [1,2,3]
enum = a.to_enum
a.replace([])
enum.to_a
  #=> [] 

顺便说一句,我用习惯的记法#=>表示什么方法returns,#->表示打印什么

loop do

一起使用

假设:

enum = [1,2,3].to_enum
  #=> #<Enumerator: [1, 2, 3]:each>

我们可以通过调用 Enumerator#next 逐步执行 enum:

enum.next #=> 1 
enum.next #=> 2 
enum.next #=> 3 
enum.next #=> StopIteration: iteration reached an end
enum.rewind 
enum.next #=> 1

如您所见,当我们试图超出枚举数的末尾时会引发 StopIteration 异常。

Kernel#loop 与枚举器一起使用通常很方便,因为 loop 通过跳出循环来处理 StopIteration 异常。例如:

enum = [1,2,3].to_enum
loop do
  puts enum.next
end
  #-> 1
  #   2
  #   3
  #=> nil 

您的 names 方法,已简化

您考虑的示例有点混乱,因为正在修改 @names ("mutated")。让我们从一个更简单的例子开始:

def names
  yield "Lucy"
  s = "Billy-Bob"
  yield s
end

如果我们用一个块来执行这个,那就不足为奇了:

def names
  yield "Lucy"
  s = "Billy-Bob"
  yield s
end

names { |s| puts "My name is #{s}" }
  #-> My name is Lucy
  #   My name is Billy-Bob

现在让我们为方法创建一个枚举器:

enum = to_enum(:names)
  #=> #<Enumerator: main:names> 

我们可以通过重复调用 Enumerator#next:

来检查枚举器的内容
enum.next #=> "Lucy" 
enum.next #=> "Billy-Bob" 
enum.next #=> StopIteration: iteration reached an end (exception)

你看到发生了什么事了吗? Ruby 正在单步执行方法 names 并计算每次调用 yield 时传递给块的参数。

我们可以在 enum 上调用 each,使用与之前相同的块:

enum.each { |s| puts "My name is #{s}" }
  #-> My name is Lucy
  #   My name is Billy-Bob

each 只是将 enum 的每个元素传递给块。

你的方法names,终于

现在让我们看看您给出的具体示例。

@names = %w[Ylva Brighid Shifra Yesamin]

def names
  yield @names.shift
  yield @names.shift
  yield @names.shift
  yield @names.shift
end

你知道你可以用一个块调用 names:

names { |s| puts "My name is #{s}" }
  #-> My name is Ylva
  #   My name is Brighid
  #   My name is Shifra
  #   My name is Yesamin

之后:

@names #=> []

让我们重新初始化 @names:

@names = %w[Ylva Brighid Shifra Yesamin]

并在方法 names:

上创建一个枚举器
enum = to_enum(:names)
  #=> #<Enumerator: main:names>

现在让我们使用 next 遍历枚举器,并在每一步检查 @names 的值:

enum.next # => @names.shift => "Ylva"
  # => "Ylva"

next 导致 Ruby 转到 names 中的第一个 yield 并计算和 return 要传递给块的参数。正如预期的那样:`

@names    #=> ["Brighid", "Shifra", "Yesamin"]

再来三遍:

enum.next #=> "Brighid" 
@names    #=> ["Shifra", "Yesamin"] 

enum.next #=> "Shifra" 
@names    #=> ["Yesamin"] 

enum.next #=> "Yesamin" 
@names    #=> [] 

再试一次:

enum.next #StopIteration: iteration reached an end

所有这些现在都应该说得通了,但有些事情可能会让您大吃一惊:

enum.to_a
  #=> [nil, nil, nil, nil]

那是因为:

[][0]   #=> nil
[][1]   #=> nil
[][999] #=> nil

现在让我们使用我们之前使用的块将 each 发送到 enum:

@names = %w[Ylva Brighid Shifra Yesamin]
enum.each { |s| puts "My name is #{s}" }
  #-> My name is Ylva
  #   My name is Brighid
  #   My name is Shifra
  #   My name is Yesamin

您是否注意到,虽然我们需要重新初始化 @names,但我们当然不必重新创建枚举器?

一切正常。在调用 enum.next 时,它调用 names 方法的第一行,然后 yields 给调用者,即在该点停止 names 方法的执行流程。在下一次调用 enum.next 时,执行流程将从它停止的地方开始。

Ruby 实际上有一个名为 Fiber 的对象,它可以更简洁地证明这一点:http://apidock.com/ruby/Fiber 它们允许您通过调用 [=11] 在程序中的任意点 'pause execution' =] 和 resume 您稍后离开的地方。

例如上面的例子:

@names = %w[Ylva Brighid Shifra Yesamin]
fiber = Fiber.new do
  Fiber.yield @names.shift # yields control to the caller
  Fiber.yield @names.shift
  Fiber.yield @names.shift
  Fiber.yield @names.shift
end

# the resume calls give control to the fiber at the point we left off
puts fiber.resume #=> Ylva
puts fiber.resume #=> Brighid
puts fiber.resume #=> Shifra
puts fiber.resume #=> Yesamin