Ruby中Enumerator类的目的是什么?

时间:2013-06-06 23:01:52

标签: ruby enumerator

如果我像这样创建一个Enumertor:

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

enum是一名调查员。这个对象的目的是什么?我不能这样说:

enum { |i| puts i }

但我可以这样说:

enum.each { |i| puts i }

这似乎是多余的,因为Enumerator是使用.each创建的。它似乎存储了一些有关each方法的数据。

我不明白这里发生了什么。我确信我们有这个Enumerator类有一些合乎逻辑的原因,但是它能做什么,Array不能呢?我想也许它是Array和其他Enumerables的祖先,但它似乎不是。究竟是什么原因导致Enumerator类存在,以及它将在何种上下文中使用?

5 个答案:

答案 0 :(得分:4)

如果你做enum = [1,2,3].each; enum.next会怎样?:

enum = [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>
enum.next
=> 1
enum.next
=> 2
enum.next
=> 3
enum.next
StopIteration: iteration reached an end

当您有一个执行计算的枚举器(如素数计算器或Fibonacci序列生成器)时,这非常有用。它为您编写代码提供了灵活性。

答案 1 :(得分:1)

我认为,主要目的是按要求获取元素,而不是将它们全部放在一个循环中。我的意思是这样的:

e = [1, 2, 3].each
... do stuff ...
first = e.next
... do stuff with first ...
second = e.next
... do more stuff with second ...

请注意,那些do stuff部分可以在远离彼此的不同功能中。

Lazily评估无限序列(例如素数,斐波纳契数,像'a'..'z','aa'..'az','ba'..'zz','aaa'..这样的字符串键等)是枚举器的一个很好的用例。

答案 2 :(得分:1)

到目前为止,当您想要迭代一系列可能无限长的数据时,Enumerator会派上用场。

以扩展Enumerator的素数生成器prime_generator为例。如果我们想得到前5个素数,我们可以简单地写prime_generator.take 5而不是嵌入&#34;限制&#34;进入生成逻辑。因此,我们可以分离生成素数并从生成的素数中取一定数量,使生成器可重复使用。

我喜欢使用Enumerable返回枚举器的方法链接方法,如下例所示(它可能不是&#34;目的&#34;但我想指出它的美学方面):

prime_generator.take_while{|p| p < n}.each_cons(2).find_all{|pair| pair[1] - pair[0] == 2}

这里prime_generator是Enumerator的一个实例,它逐个返回素数。我们可以使用n Enumerable方法获取take_while以下的素数。方法each_consfind_all都返回枚举器,因此可以链接它们。此示例旨在生成n以下的孪生素数。这可能不是一种有效的实现方式,但可以很容易地在一行中编写,适用于原型设计的恕我直言。

以下是基于枚举器的prime_generator非常直接的实现:

def prime?(n)
  n == 2 or
    (n >= 3 and n.odd? and (3...n).step(2).all?{|k| n%k != 0})
end
prime_generator = Enumerator.new do |yielder|
  n = 1
  while true
    yielder << n if prime? n
    n += 1
  end
end

答案 3 :(得分:0)

可以组合枚举器:

array.each.with_index { |el, idx| ... }

答案 4 :(得分:0)

要了解枚举器类的主要优点,您首先需要区分内部和外部迭代器。使用内部迭代器,迭代器本身控制迭代。使用外部迭代器,客户端(通常是程序员)通常控制迭代。使用外部迭代器的客户端必须推进遍历并从迭代器中显式请求下一个元素。相反,客户端将内部迭代器交给要执行的操作,然后迭代器将该操作应用于集合中的每个元素。

在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类,在我们的示例中,enum局部变量包含一个Enumerator实例。然后,我们使用外部迭代遍历我们创建的枚举方法。我们的枚举方法称为“种类”,请注意,我们使用的yield方法通常是对块执行的。在此,枚举器一次将产生一个值。每次屈服后暂停。当要求另一个值时,它将在上一个屈服值之后立即恢复,并执行到下一个屈服值。当什么都没有产生时,您接下来调用它,它将调用StopIteration异常。

那么Ruby中外部迭代的功能是什么?有几个好处,我将重点介绍其中一些。首先,Enumerator类允许链接。例如,在Enumerator类中定义了with_index,它允许我们在遍历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实例方法好东西可供我们使用,例如chunkdrop_whileflat_mapgreplazypartitionreducetake_while等。

f.partition {|kind| kind =~ /^a/ }
 => [["apple"], ["orange", "pear", "banana"]] 

有趣的是,Enumerable模块的每个实例方法实际上都是在幕后调用我们的每个方法以获取可枚举的项。因此,如果我们要实现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)

所以现在我们可以将每个方法修改为使用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实例,可以使用客户端代码进行控制,以备后用。