【问题标题】:Mimic another Ruby class so the object passes the === type check模仿另一个 Ruby 类,使对象通过 === 类型检查
【发布时间】:2011-10-21 20:12:22
【问题描述】:

我想创建一个充当特定类的对象,例如 Fixnum,但它既不是该类的实例,也不是它的子类。

这有多种用例。在 Fixnum 的情况下,我想定义一个更具体的整数类型,它本质上是一个 Fixnum,但也实现了一些额外的逻辑。我不能对 Fixnum 本身进行子类化,因为不能对 Fixnum 和 Symbol 等直接类型进行子类化。

另一个用例是自动化测试中的模拟:有时您想创建一个对象,该对象的行为类似于某个类(通常是模型实例),但由于技术原因不是该确切类的实例。

以下是如何创建将所有方法委托给内部存储的固定编号的特定整数类型:

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

我的问题是如何创建一个通过=== 检查的模拟类型而不增加内置类上的=== 方法?

【问题讨论】:

  • 注意:我在这里询问 MRI。不同的 Ruby 实现对 === 的实现方式不同,因此一个解决方案可能不适用于其他解决方案。
  • “我不能继承 Fixnum 本身,因为像 Fixnum 和 Symbol 这样的直接类型不能被继承。”这是为什么?我只是尝试将 Fixnum 子类化,它在 1.8.7 和 1.9.2 中工作
  • @jigfox,现在尝试做Mynum.new 并看着它失败。直接类型在内部以一种阻止它们的子类可用的方式表示;以these checks 为例。

标签: ruby class types mocking


【解决方案1】:

如果我们谈论的是 MRI1,答案很简单:你不能。

Module#=== 方法实际上是 aliasrb_obj_is_kind_of C 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;
}

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

后一种检查是必需的,因为 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] 

这里,ObjectBasicObject 将通过第一次检查成功,Kernel 通过第二次检查。

即使您尝试创建(使用 C 扩展名)一个代理对象来尝试欺骗 rb_obj_is_kind_of 方法,它也需要与真正的 Fixnum 具有相同的方法表,这将有效包括所有Fixnum 的方法。


1 我研究了 Ruby 1.9 的内部结构,但它们在 1.8 中的行为方式完全相同。

【讨论】:

  • 很好的答案。如果您查看我自己的答案并将您的 cmets 添加到其中,我将不胜感激。
  • 我不确定这是否是正确的答案,因为 Fixnum 不使用Module.===,而是使用Fixnum.===numeric.c 中有一个非常具体的覆盖。
  • @John,你能告诉我那个方法吗?我只能看到Fixnum#===。此外,您似乎忽略了case 的实际工作方式...检查this sn-p。
  • @whitequark:我指的是github.com/ruby/ruby/blob/trunk/numeric.c#L2761 处的fix_equal 方法,我相信它已被使用,因为它有一个rb_define_methodrb_define_method(rb_cFixnum, "===", fix_equal, 1);。另外,您提供的链接对我不起作用。
  • @John,该链接对我有用,但无论如何,这里是 same snippet on another pastebin site。不使用此方法;它是为 Fixnum 实例定义的,而不是类本身。我希望我的代码能解释你的误解。
【解决方案2】:

这是我在问题中警告过的一个骇人听闻的解决方案:

Fixnum === page  #=> false

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

Fixnum === page  #=> true

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

【讨论】:

  • 实际上,你没有。 important 方法是 C API 中的方法,它在内部确实被使用了数百次(嗯……准确地说是 208 次)。您正在重新定义的是案例比较方法;你唯一可能打破的是,好吧,案例比较。这个解决方案对我来说看起来非常好。谈到其他实现,我认为 JRuby 的工作方式大致相同,但对于 Rubinius,您应该仔细检查所有内容。
猜你喜欢
  • 2021-10-27
  • 1970-01-01
  • 2018-10-16
  • 2011-06-02
  • 2013-11-28
  • 1970-01-01
  • 1970-01-01
  • 2012-05-07
  • 1970-01-01
相关资源
最近更新 更多