【问题标题】:What prevents me from including a class in Ruby?是什么阻止我在 Ruby 中包含一个类?
【发布时间】:2020-05-03 02:31:03
【问题描述】:

我正在尝试了解一些 Ruby 内部结构:

尝试使用include 一个 而不是一个模块,会导致TypeError:(这是设计使然)

class C
end

class Foo
end

Foo.include(C)
#=> TypeError: wrong argument type Class (expected Module)

我想知道这种类型检查是如何“在后台”工作的。

由于类模块,我假设Ruby会检查参数是否是Module的实际实例:

C.is_a?(Module)        #=> true
C.instance_of?(Module) #=> false

听起来很合理,不是吗?

但是当我定义自己的 Module 子类并创建该子类的实例时,它就可以正常工作了:

class Klass < Module
end

K = Klass.new

Foo.include(K)
# no error

但是KKlass 的一个实例,就像CClass 的一个实例一样。而KlassModule 的子类,就像Class

K.is_a?(Module)        #=> true
K.instance_of?(Module) #=> false

K.class #=> Klass
C.class #=> Class

Klass.superclass #=> Module
Class.superclass #=> Module

那么include 中的那种类型检查实际上做了什么?

是否有一个隐藏属性允许 Ruby 将模块与类区别开来?

由于这是特定于实现的:我对 YARV/MRI 特别感兴趣。

【问题讨论】:

  • 也许它直接检查这是否是Class的实例? K.is_a?(Class) #=&gt; false, C.is_a?(Class) #=&gt; true.
  • @MarekLipka 在 C 源代码中我看到 Check_Type(module, T_MODULE),即明确检查 T_MODULE 但没有像“not T_CLASS”这样的逆向

标签: ruby class include


【解决方案1】:

我在这里回答我自己的问题


是否有一个隐藏属性允许 Ruby 将模块与类区别开来?

确实有。在内部,所有 Ruby 对象都以一个名为 RBasic 的结构开始:

struct RBasic {
    VALUE flags;
    const VALUE klass;
};

RBasic 中我们有flags,这些标志包含类型信息:

enum ruby_value_type {
    RUBY_T_NONE = 0x00,

    RUBY_T_OBJECT = 0x01,
    RUBY_T_CLASS = 0x02,
    RUBY_T_MODULE = 0x03,
    RUBY_T_FLOAT = 0x04,
    RUBY_T_STRING = 0x05,
    // ...

    RUBY_T_MASK = 0x1f
};

这就是 Ruby 在进行类型检查时最终要检查的内容:

#define RB_BUILTIN_TYPE(x) (int)(((struct RBasic*)(x))->flags & RUBY_T_MASK)

RB_BUILTIN_TYPE 也被Marshal 用来转储类型信息:

module M ; end
class C ; end

Marshal.dump(M) #=> "\x04\bm\x06M"
Marshal.dump(C) #=> "\x04\bc\x06C"
Marshal.dump(4) #=> "\x04\bi\t"
#                          ^
#              m = module, c = class, i = integer

在 Ruby 中,我们可以通过 Fiddle 检查内部类型:

require 'fiddle'

def type(obj)
  struct = Fiddle::Pointer.new(obj.object_id << 1)
  flags = struct[0]
  flags & 0x1f
end

module M ; end
class C ; end

type(M) #=> 3   (RUBY_T_MODULE = 0x03)
type(C) #=> 2   (RUBY_T_CLASS = 0x02)

由于 Fiddle 还允许修改底层数据,我们可以通过相应地更改其标志将一个类变成一个模块...

让我们试一试:

class C
  def hello
    'hello from class'
  end
end

class Foo
end

Foo.include(C)
#=> TypeError: wrong argument type Class (expected Module)

现在类型从0x02(类)更改为0x03(模块):

require 'fiddle'

struct = Fiddle::Pointer.new(C.object_id << 1)
struct[0] = (struct[0] & ~0x1f) | 0x03

Foo.include(C)
# NoMethodError: undefined method `append_features' for C:Class

仍然是一个错误,但 Ruby 不再抱怨类型了!

显然,Class 取消定义 Module#append_features 因为该方法对类没有多大意义。让我们为C重新定义它:

C.define_singleton_method(:append_features, Module.instance_method(:append_features))

Foo.include(C)
# no error!

Foo.ancestors
#=> [Foo, C, Object, BasicObject, Object, Kernel, BasicObject]

Foo.new.hello
#=> "hello from class"

我们开始了:一个包含在另一个类中的类。

注意:我在这里摆弄 Ruby 的内部结构。不要在生产中使用这种黑客。您已收到警告。

【讨论】:

    【解决方案2】:

    正如@Stefan 评论的那样,Module#include 调用宏 Check_Type(module, T_MODULE)。你可以在https://ruby-doc.org/core-2.6/Module.html#method-i-include找到这个

    进一步挖掘源码,可以发现在头文件ruby.h中,有一行

    #define Check_Type(v,t) rb_check_type((VALUE)(v),(t))
    

    所以Check_Type只是rb_check_type的一个方便的别名,你可以在error.c中找到rb_check_type的定义:

    void
    rb_check_type(VALUE x, int t)  
    { 
        int xt;                    
    
        if (x == Qundef) {         
      rb_bug(UNDEF_LEAKED);        
        }
    
        xt = TYPE(x);              
        if (xt != t || (xt == T_DATA && RTYPEDDATA_P(x))) {
      unexpected_type(x, xt, t);   
        }
    } 
    

    int t 是类型的唯一“ID”,int xtx 的实际类型的 ID。你可以看到if (xt != t || ...),所以Check_Type 正在检查类型等价,而不是is-a 关系。

    TL;DR

    Ruby 检查包含的模块是否实际上是一个模块而不是一个类。

    【讨论】:

      猜你喜欢
      • 2016-11-25
      • 1970-01-01
      • 1970-01-01
      • 2012-05-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多