要了解枚举器类的主要优势,首先需要区分内部和外部迭代器。对于内部迭代器,迭代器本身控制迭代。使用外部迭代器,客户端(通常是程序员)控制迭代。使用外部迭代器的客户端必须推进遍历并从迭代器显式请求下一个元素。相反,客户端将一个操作交给内部迭代器执行,而迭代器将该操作应用于集合中的每个元素。
在 Ruby 中,Enumerator 类使您能够使用外部迭代器。一旦你了解了外部迭代器,你就会开始发现很多优势。首先,让我们看看 Enumerator 类如何促进外部迭代:
class Fruit
def initialize
@kinds = %w(apple orange pear banana)
end
def kinds
yield @kinds.shift
yield @kinds.shift
yield @kinds.shift
yield @kinds.shift
end
end
f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
=> "apple"
f.instance_variable_get :@kinds
=> ["orange", "pear", "banana"]
enum.next
=> "orange"
f.instance_variable_get :@kinds
=> ["pear", "banana"]
enum.next
=> "pear"
f.instance_variable_get :@kinds
=> ["banana"]
enum.next
=> "banana"
f.instance_variable_get :@kinds
=> []
enum.next
StopIteration: iteration reached an end
需要注意的是,在对象上调用 to_enum 并传递与方法对应的符号将实例化 Enumerator 类,在我们的示例中,枚举局部变量包含一个 Enumerator 实例。然后我们使用外部迭代来遍历我们创建的枚举方法。我们的枚举方法称为“kinds”,注意我们使用了 yield 方法,我们通常使用块。在这里,枚举器一次将产生一个值。它在每次产量后暂停。当要求另一个值时,它将在最后一个产生的值之后立即恢复,并执行到下一个产生的值。当没有任何东西可以让出时,您调用 next 时,它将调用 StopIteration 异常。
那么 Ruby 中外部迭代的威力是什么?有几个好处,我将重点介绍其中的一些。首先,Enumerator 类允许链接。例如, with_index 在 Enumerator 类中定义,它允许我们在迭代 Enumerator 对象时指定迭代的起始值:
f.instance_variable_set :@kinds, %w(apple orange pear banana)
enum.rewind
enum.with_index(1) do |name, i|
puts "#{name}: #{i}"
end
apple: 1
orange: 2
pear: 3
banana: 4
其次,它从 Enumerable 模块中提供了大量有用的便利方法。请记住 Enumerator 是一个类,而 Enumerable 是一个模块,但 Enumerable 模块包含在 Enumerator 类中,因此 Enumerators 是 Enumerable:
Enumerator.ancestors
=> [Enumerator, Enumerable, Object, Kernel, BasicObject]
f.instance_variable_set :@kinds, %w(apple orange pear banana)
enum.rewind
enum.detect {|kind| kind =~ /^a/}
=> "apple"
enum
=> #<Enumerator: #<Fruit:0x007fb86c09bdf8 @kinds=["orange", "pear", "banana"]>:kinds>
Enumerator 的另一个主要好处可能还不是很清楚。让我通过一个演示来解释这一点。您可能知道,您可以通过包含 Enumerable 模块并定义每个实例方法来使任何用户定义的类 Enumerable:
class Fruit
include Enumerable
attr_accessor :kinds
def initialize
@kinds = %w(apple orange pear banana)
end
def each
@kinds.each { |kind| yield kind }
end
end
这很酷。现在我们有大量的 Enumerable 实例方法可供我们使用,例如chunk
, drop_while
, flat_map
, grep
, lazy
, partition
,reduce
等等take_while
。
f.partition {|kind| kind =~ /^a/ }
=> [["apple"], ["orange", "pear", "banana"]]
有趣的是,Enumerable 模块的每个实例方法实际上都是在后台调用我们的 each 方法以获取可枚举项。因此,如果我们要实现 reduce 方法,它可能看起来像这样:
module Enumerable
def reduce(acc)
each do |value|
acc = yield(acc, value)
end
acc
end
end
注意它是如何将一个块传递给每个方法的,因此我们的每个方法都应该向块返回一些东西。
但是看看如果客户端代码在没有指定块的情况下调用 each 方法会发生什么:
f.each
LocalJumpError: no block given (yield)
所以现在我们可以修改我们的 each 方法以使用 enum_for,它会在没有给出块时返回一个 Enumerator 对象:
class Fruit
include Enumerable
attr_accessor :kinds
def initialize
@kinds = %w(apple orange pear banana)
end
def each
return enum_for(:each) unless block_given?
@kinds.each { |kind| yield kind }
end
end
f = Fruit.new
f.each
=> #<Enumerator: #<Fruit:0x007ff70aa3b548 @kinds=["apple", "orange", "pear", "banana"]>:each>
现在我们有一个 Enumerator 实例,我们可以用我们的客户端代码控制以供以后使用。