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

时间:2015-04-10 01:22:03

标签: ruby enumerator

(例子来自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应为空。这个神奇的(部分调用方法)怎么会发生?

2 个答案:

答案 0 :(得分:1)

<强>定义

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

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

方法&#34;创建一个新的枚举器,通过调用method上的obj进行枚举,传递args,如果有的话。由于此方法是在Object上创建的,因此可以在任何对象上调用它,但如果未在对象的类上定义each,则没有必要这样做:

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
  #=> [] 

顺便说一下,我使用惯用符号#=>来表示方法返回的内容,并使用#->来表示打印的内容。

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正在被修改(&#34;变异&#34;)。让我们从一个更简单的例子开始:

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时传递给块的参数。

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

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转到yield中的第一个names并计算并返回要传递给块的参数。正如所料:`

@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,但当然,我们不必重新创建枚举器?

答案 1 :(得分:1)

它按预期工作。在调用enum.next中,它调用names方法中的第一行,然后输出到调用者,即在此时停止执行names方法的流程。在下一次调用enum.next时,执行流程将从它停止的位置获取。

Ruby实际上有一个名为Fiber的对象,它可以更简洁地证明这一点:http://apidock.com/ruby/Fiber它们允许您通过调用Fiber.yield和{{1}在程序中的任意点“暂停执行”你以后离开的地方。

例如,上面的示例:

resume