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