我想创建一个充当特定类的对象,例如Fixnum,但它不是该类的实例,也不是它的子类。
有各种用例。在Fixnum的情况下,我想定义一个更具体的整数类型,它本质上是一个Fixnum,但也实现了一些额外的逻辑。我不能将Fixnum本身子类化,因为不能将诸如Fixnum和Symbol之类的直接类型子类化。
另一个用例是在自动化测试中进行模拟:有时你想要创建一个像某个类(通常是模型实例)的对象,但出于技术原因而不是那个确切类的实例。
以下是如何创建一个特定的整数类型,将所有方法委托给内部存储的fixnum:
require 'delegate'
require 'forwardable'
# integer representing a page number
class PageNumber < DelegateClass(Integer)
extend Forwardable
def initialize(value, name)
@name = name
super(value)
end
def inspect
"#{@name} #{to_i}"
end
alias_method :to_i, :__getobj__
def_delegators :to_i, :instance_of?, :kind_of?, :is_a?
end
此对象可以传递is_a?
和类似的检查:
page = PageNumber.new(1, "page")
page.is_a? Fixnum #=> true
但我所做的一切都无法通过Module#===
类型检查:
# my problem:
Fixnum === page #=> false
我的对象未通过此检查这一事实非常不幸,因为===
语句内部使用了case
方法:
case page
when Fixnum
# it will never get here
when String
# ...
else
# ...
end
我的问题是如何创建一个模拟类型,通过===
检查而不用在内置类中增加===
方法?
答案 0 :(得分:15)
如果我们谈论MRI 1 ,答案很简单:你做不到。
Module#===
方法实际上是alias个rb_obj_is_kind_of
C API方法的{{3}}。后者的实现非常简短,我将它贴在这里:
VALUE
rb_obj_is_kind_of(VALUE obj, VALUE c)
{
VALUE cl = CLASS_OF(obj);
/* Type checking of `c' omitted */
while (cl) {
if (cl == c || RCLASS_M_TBL(cl) == RCLASS_M_TBL(c))
return Qtrue;
cl = RCLASS_SUPER(cl);
}
return Qfalse;
}
正如您所看到的,此方法遍历被检查对象的祖先,并以两种方式对它们进行比较:首先,它检查祖先是否与传递的模块相同,然后检查它们是否相同有相同的方法表。
后一项检查是必需的,因为Ruby中包含的模块似乎插入了继承链中,但由于一个模块可能包含在其他几个模块中,因此它不是插入链中的真实模块,而是代理对象,它的常量和方法表指向原始模块。
例如,让我们看一下Object的祖先:
ruby-1.9.2-p136 :001 > Object.ancestors
=> [Object, Kernel, BasicObject]
ruby-1.9.2-p136 :002 > Object.ancestors.map { |mod| Object.new.is_a? mod }
=> [true, true, true]
此处,第一次检查会成功比较Object
和BasicObject
,第二次检查会成功比较Kernel
。
即使你试图(使用C扩展名)试图欺骗rb_obj_is_kind_of
方法的代理对象,它也需要具有与真实Fixnum
相同的方法表。 ,这将有效地包括所有Fixnum
的方法。
<小时/> 1 我已经研究过Ruby 1.9的内部结构,但它们在1.8中的行为完全相同。
答案 1 :(得分:3)
这是一个我在我的问题中警告过的黑客解决方案:
Fixnum === page #=> false
Numeric.extend Module.new {
def ===(obj)
obj.instance_of?(PageNumber) or super
end
}
Fixnum === page #=> true
它解决了这个问题,但提出了一个问题,这样做是否安全?我无法从头脑中想到这种方法的任何缺点,但由于我们在这里弄乱了一个非常重要的方法,它可能不是我们想要做的事情。