【问题标题】:What is the purpose of the Enumerator class in RubyRuby 中 Enumerator 类的用途是什么
【发布时间】:2018-10-29 08:05:06
【问题描述】:

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

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

enum 是一个枚举器。这个对象的目的是什么?我不能这样说:

enum { |i| puts i }

但我可以这样说:

enum.each { |i| puts i }

这似乎是多余的,因为枚举器是用.each 创建的。似乎它正在存储一些关于 each 方法的数据。

我不明白这里发生了什么。我确信我们有这个 Enumerator 类有一些合乎逻辑的原因,但是它可以做什么,而 Array 不能呢?我以为它可能是 Array 和其他 Enumerables 的祖先,但似乎不是。 Enumerator 类存在的具体原因是什么,它会在什么情况下使用?

【问题讨论】:

  • This article 展示了一些使用枚举器进行惰性求值的示例。

标签: ruby enumerator


【解决方案1】:

如果你这样做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

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

【讨论】:

    【解决方案2】:

    我认为,主要目的是按需获取元素,而不是在一个循环中获取所有元素。我的意思是这样的:

    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'.. 等字符串键)是枚举器的一个很好的用例。

    【讨论】:

      【解决方案3】:

      到目前为止,当您想要遍历可能无限长的数据序列时,枚举器会派上用场。

      以扩展 Enumerator 的素数生成器prime_generator 为例。如果我们想得到前 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 的一个实例,它一个一个地返回素数。我们可以使用 Enumerable 的 take_while 方法获取低于 n 的素数。 each_consfind_all 方法都返回 Enumerator 以便它们可以被链接。此示例旨在生成低于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
      

      【讨论】:

        【解决方案4】:

        可以组合枚举数:

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

        【讨论】:

          【解决方案5】:

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

          在 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
          

          这很酷。现在我们有大量的可枚举实例方法可供我们使用,例如chunkdrop_whileflat_mapgreplazypartitionreducetake_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 方法的,因此我们的 each 方法应该yield返回块。

          但是看看如果客户端代码在没有指定块的情况下调用 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 实例,我们可以用我们的客户端代码控制以供以后使用。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-04-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多