我有一个代表集合的类。我加入了 Enumerable
模块中,并定义了方法 #each
这样我就可以得到它的所有方法。
但问题是 Enumerable
'的方法不保持同一个类。所以,举例来说,如果我的类名为 Collection
如果我这样做 Collection#select
,我希望结果的类也是 Collection
(而不是 Array
). 有什么方法可以实现这个目标吗?
不幸的是,Ruby的集合操作不是类型保存的。每个集合操作总是返回一个 Array
.
对于收藏品,如 Set
或 Tree
s,这仅仅是烦人的,因为你需要总是将它们转换回你想要的类型。但是对于一个无限的所有质数的懒惰流来说,这是灾难性的:你的程序要么挂起,要么耗尽内存,试图构造一个无限大的 Array
.
大多数Collection API要么消除重复代码,要么保留类型,但不是两者兼而有之。例如,.NET的Collection API大多消除了重复的代码,但它总是返回相同的类型。IEnumerable
(相当于Ruby的 Enumerator
). Smalltalk的Collection API是保留类型的,但它是通过在每个Collection类型中重复所有的Collection操作来实现的。
唯一一个既保留类型又消除重复的Collection API是Scala的。它通过引入新的概念,即 收集建设者,它知道如何有效地构造一个特定类型的集合。Collection Operations是以Collection Builders来实现的,只有Collection Builders需要被复制......但无论如何,这些都是每个Collection所特有的。
如果你想在Ruby中使用类型保存的Collection操作,你需要在自己的Collection中复制所有的Collection操作(这将仅限于你自己的代码),或者重新设计整个Collection API来使用Builders(这不仅需要重新设计你自己的代码,还需要重新设计现有的Collection,包括每一个第三方的Collection)。
很明显,第二种方法即使不是不可能,至少也是不切实际的。不过第一种方法也有它的问题。集合操作是... 预期 返回 Array
的,违反这个期望可能会破坏其他人的代码!
你可以采取类似于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
Since Enumerable#select
被设计成返回一个数组,你需要告诉某个地方如何将其映射到一个 Collection
实例。这意味着,您需要明确定义 Collection#select
. 否则,Ruby将无法从原始数组结果中知道映射规则的 Enumerable#select
到 Collection
实例。
另一种方法是将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
的代理。
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