6

如果我像这样创建一个枚举器:

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 类存在的确切原因是什么,它会在什么情况下使用?

4

5 回答 5

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

当您有一个进行计算的枚举器(例如素数计算器或斐波那契序列生成器)时,这可能很有用。它为您编写代码的方式提供了灵活性。

于 2013-06-06T23:07:54.910 回答
1

正如到目前为止所回答的那样,当您想要遍历可能无限长的数据序列时,Enumerator 会派上用场。

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

我喜欢使用 Enumerable 返回 Enumerator 的方法进行方法链接,如下例所示(它可能不是“目的”,但我只想指出它的美学方面):

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

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

这是一个prime_generator基于 Enumerator 的非常简单的实现:

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
于 2013-06-07T10:57:49.277 回答
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部分可以具有不同的功能,彼此相距很远。

惰性求值的无限序列(例如素数、斐波那契数、字符串键'a'..'z','aa'..'az','ba'..'zz','aaa'..等)是枚举器的一个很好的用例。

于 2013-06-06T23:12:00.263 回答
0

可以组合枚举器:

array.each.with_index { |el, idx| ... }
于 2013-06-07T08:11:21.920 回答
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 类,在我们的示例中,枚举局部变量包含一个 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 实例,我们可以用我们的客户端代码控制以供以后使用。

于 2018-10-28T23:55:07.667 回答