模仿另一个Ruby类,以便对象传递===类型检查

mis*_*lav 10 ruby types class mocking

我想创建一个充当特定类的对象,例如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
Run Code Online (Sandbox Code Playgroud)

此对象可以通过is_a?和类似的检查:

page = PageNumber.new(1, "page")
page.is_a? Fixnum  #=> true
Run Code Online (Sandbox Code Playgroud)

但我没做什么可以让它通过Module#===类型检查:

# my problem:
Fixnum === page    #=> false
Run Code Online (Sandbox Code Playgroud)

我的对象未通过此检查这一事实非常不幸,因为该===方法在case语句内部使用:

case page
when Fixnum
  # it will never get here
when String
  # ...
else
  # ...
end
Run Code Online (Sandbox Code Playgroud)

我的问题是如何在增加内置类的方法的情况下创建一个传递===检查的模拟类型?===

whi*_*ark 15

如果我们谈论MRI 1,答案很简单:你做不到.

Module#===方法有效地是一个别名rb_obj_is_kind_ofC API方法.后者的实现是如此之短,我将它贴在这里:

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;
}
Run Code Online (Sandbox Code Playgroud)

如您所见,此方法遍历被检查对象的祖先,并以两种方式对它们进行比较:首先,它检查祖先是否与传递的模块相同,然后检查它们是否具有相同的方法表.

后一项检查是必需的,因为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] 
Run Code Online (Sandbox Code Playgroud)

在这里,Object并且BasicObject将成功通过第一检查相比较,Kernel由第二个.

即使你试图(使用C扩展名)试图欺骗该rb_obj_is_kind_of方法的代理对象,它也需要具有与真实相同的方法表Fixnum,这将有效地包括所有Fixnum方法.


1我已经调查了Ruby 1.9的内部结构,但它们在1.8中的表现方式完全相同.


mis*_*lav 3

这是我在问题中警告反对的一种黑客解决方案:

Fixnum === page  #=> false

Numeric.extend Module.new {
  def ===(obj)
    obj.instance_of?(PageNumber) or super
  end
}

Fixnum === page  #=> true
Run Code Online (Sandbox Code Playgroud)

它解决了问题,但提出了一个问题,这样做安全吗?我无法从我的脑海中想到这种方法的任何缺点,但由于我们在这里搞乱了一个非常重要的方法,所以它可能不是我们想要做的事情。