5

我有一个代表集合的类。我将Enumerable模块包含在其中并定义了方法#each,以便获得它的所有方法。

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

4

4 回答 4

6

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

于 2012-10-27T00:04:18.593 回答
4

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

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

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

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

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

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

您可以采用类似于 Ruby 2.0 的惰性集合操作的方法:您可以preserve_type向 API 添加一个新方法,该方法返回一个具有类型保留集合操作的代理对象。这样,与标准 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
于 2012-10-27T03:26:26.730 回答
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]>
于 2012-10-27T02:04:16.540 回答
2

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

  include Enumerable

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

  def reject(&block)
    self.class.new(super.reject(&block))
  end
于 2020-05-15T15:35:17.227 回答