【问题标题】:Ruby enumerables don't keep the same classRuby 可枚举不保持同一个类
【发布时间】:2012-10-17 05:58:18
【问题描述】:

我有一个代表集合的类。我在其中包含了Enumerable模块并定义了方法#each,这样我就得到了它的所有方法。

但问题是Enumerable 的方法不保持同一个类。因此,例如,如果我的班级名为Collection,如果我命名为Collection#select,我希望结果的班级也是Collection(而不是Array)。有没有办法做到这一点?

【问题讨论】:

  • 您需要手动覆盖它。

标签: ruby enumerable


【解决方案1】:

由于Enumerable#select 旨在返回一个数组,因此您需要在某处说明如何将其映射到Collection 实例。这意味着,您需要明确定义Collection#select。否则 Ruby 将不知道从 Enumerable#select 的原始数组结果到 Collection 实例的映射规则。

【讨论】:

    【解决方案2】:

    不幸的是,Ruby 的集合操作不是类型保留的。每个收集操作总是返回一个Array

    对于像Sets 或Trees 这样的集合,这只是烦人的,因为您需要始终将它们转换回您想要的类型。但例如对于所有素数的无限惰性流,这是灾难性的:您的程序将挂起或内存不足,试图构造一个无限大的Array

    大多数集合 API 要么消除重复代码,要么保留类型,但不能同时使用两者。例如。 .NET 的 Collection API 主要消除了重复代码,但它总是返回相同的类型:IEnumerable(相当于 Ruby 的 Enumerator)。 Smalltalk 的 Collection API 是类型保留的,但它通过在每个 Collection 类型中复制所有 Collection 操作来实现这一点。

    唯一可以保留类型但消除重复的 Collection API 是 Scala 的。它通过引入 Collection Builders 的新概念来实现这一点,它知道如何有效地构造特定类型的 Collection。集合操作是根据集合构建器实现的,只有集合构建器需要复制……但无论如何,这些操作都是特定于每个集合的。

    如果您想在 Ruby 中保留类型的集合操作,您需要在自己的集合中复制所有集合操作(​​这将仅限于您自己的代码),或者重新设计整个集合 API 以使用构建器(这需要不仅对您自己的代码进行了重大重新设计,而且对现有的集合(包括曾经编写的每个第三方集合)进行了重大重新设计。

    很明显,第二种方法即使不是不可能,至少也是不切实际的。不过,第一种方法也有问题:集合操作预期返回Arrays,违反预期可能会破坏其他人的代码!

    您可以采用类似于 Ruby 2.0 的惰性收集操作的方法:您可以向 API 添加一个新方法 preserve_type,该方法返回一个具有类型保留的收集操作的代理对象。这样一来,代码中就清楚地标出了与标准 API 的背离:

    c.select …               # always returns an Array
    
    c.preserve_type.select … # returns whatever the type of c is
    

    类似:

    class Hash
      def preserve_type
        TypePreservingHash.new(self)
      end
    end
    
    class TypePreservingHash
      def initialize(original)
        @original = original
      end
    
      def map(*args, &block)
        Hash[@original.map(*args, &block)
        # You may want to do something more efficient
      end
    end
    

    【讨论】:

      【解决方案3】:

      另一种方法是让 Collection 成为底层数组的代理:

      class Collection
        def initialize( items= nil )
          @items = items || []
        end
      
        def respond_to_missing?(method_name, include_private = false)
          Enumerable.instance_methods.include? method_name
        end
      
        def method_missing name, *args, &block
          if @items.respond_to? name
            res = @items.send name, *args, &block
            res.kind_of?( Array ) ? Collection.new(res) : res
          else
            super
          end
        end
      end
      

      在 IRB 中:

      col = Collection.new [1,2,3]
      => #<Collection:0x0000010102d5d0 @items=[1, 2, 3]>
      col.respond_to? :map
      => true
      col.map{|x| x * 2 }
      => #<Collection:0x000001009bff18 @items=[2, 4, 6]>
      

      【讨论】:

        【解决方案4】:

        以下内容对我有用。我发现只有过滤方法需要重新定义。如果我们重新定义所有返回Array 的方法,这包括不应重新定义的collect

          include Enumerable
        
          def select(&block)
            self.class.new(super.select(&block))
          end
        
          def reject(&block)
            self.class.new(super.reject(&block))
          end
        

        【讨论】:

          猜你喜欢
          • 2015-08-21
          • 2012-05-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多