我有一个代表集合的类。我将Enumerable
模块包含在其中并定义了方法#each
,以便获得它的所有方法。
但问题是Enumerable
's 方法不保持同一个类。因此,例如,如果我的班级名为Collection
,如果我这样做Collection#select
了,我希望结果的班级也是Collection
(而不是Array
)。有没有办法实现这一目标?
我有一个代表集合的类。我将Enumerable
模块包含在其中并定义了方法#each
,以便获得它的所有方法。
但问题是Enumerable
's 方法不保持同一个类。因此,例如,如果我的班级名为Collection
,如果我这样做Collection#select
了,我希望结果的班级也是Collection
(而不是Array
)。有没有办法实现这一目标?
由于Enumerable#select
旨在返回一个数组,因此您需要在某处告诉如何将其映射到Collection
实例。这意味着,您明确需要定义Collection#select
. 否则 Ruby 将不知道从原始数组结果Enumerable#select
到Collection
实例的映射规则。
不幸的是,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 以使用构建器(这需要重新设计不仅包括您自己的代码,还包括现有的集合,包括曾经编写的每个第三方集合)。
很明显,第二种方法如果不是不可能的话,至少是不切实际的。但是,第一种方法也有其问题:集合操作应返回Array
s,违反该预期可能会破坏其他人的代码!
您可以采用类似于 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
另一种方法是让 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]>
以下对我有用。我发现只有过滤方法需要重新定义。如果我们重新定义所有返回的方法Array
,这包括collect
不应该重新定义的方法。
include Enumerable
def select(&block)
self.class.new(super.select(&block))
end
def reject(&block)
self.class.new(super.reject(&block))
end