枚举器如何在方法执行过程中停止?
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
(示例来自 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