在 Ruby 中,模块可以被其他模块包含(这里,其他模块也包括类,因为类也是模块)。通过包含模块,可以构建出复杂的类层次结构。本文解释了当包含一个模块时,Ruby 内部都做了哪些实际的工作。

下面这段 Ruby 代码创建了 A, B, C 三个模块,C 包含 AB

A = Module.new

B = Module.new

module C
include A, B # Inclusion is done by this line
end

在模块 C 中,语句 include A, B 完成了模块包含的工作,该语句以 A, B 两个模块为作为参数,调用了模块 Cinclude 方法。一切都那么自然。

现在,把注意力放在 include 方法上,它的默认实现是 Module#include。根据 API 文档,它以相反的顺序对参数逐一调用 append_features 方法。对应的 C 函数是 rb_mod_include

static VALUE
rb_mod_include(int argc, VALUE *argv, VALUE module)
{
int i;
ID id_append_features, id_included;

CONST_ID(id_append_features, "append_features");
CONST_ID(id_included, "included");

for (i = 0; i < argc; i++)
Check_Type(argv[i], T_MODULE);
while (argc--) {
rb_funcall(argv[argc], id_append_features, 1, module);
rb_funcall(argv[argc], id_included, 1, module);
}
return module;
}

从 Ruby 源码来看,除了对参数模块调用 append_features 方法外,还紧接着调用了 included 方法。所以,文档描述得并不完整,可能还没有更新。

append_featuresincluded 都是回调方法。而 append_features 方法才是真正干活的地方,Ruby 通过这个方法将包含的逻辑转移到了被包含的模块上。这种精巧的设计能让你灵活地自定义包含模块的行为,既可以在发出包含动作的模块中重定义 include 方法,也可以从源头着手,重定义 append_features 方法。

再来看看 append_features 的默认实现 Module#append_features 都做了什么。下面是 API 文档上给出的源码:

static VALUE
rb_mod_append_features(VALUE module, VALUE include)
{
switch (TYPE(include)) {
case T_CLASS:
case T_MODULE:
break;
default:
Check_Type(include, T_CLASS);
break;
}
rb_include_module(include, module);

return module;
}

这个函数比较简单,除了类型安全检查,它只是 rb_include_module 函数的封装,我们进一步跟到 rb_include_module 函数中去看看。

void
rb_include_module(VALUE klass, VALUE module)
{
int changed = 0;

rb_frozen_class_p(klass);
if (!OBJ_UNTRUSTED(klass)) {
rb_secure(4);
}

if (!RB_TYPE_P(module, T_MODULE)) {
Check_Type(module, T_MODULE);
}

OBJ_INFECT(klass, module);

changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module);
if (changed < 0)
rb_raise(rb_eArgError, "cyclic include detected");
if (changed) rb_clear_cache();
}

同样,rb_include_module 函数也没有做多少实际的工作,只是做了一些安全方面的检查,然后把主要工作交给了 include_modules_at 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static int
include_modules_at(const VALUE klass, VALUE c, VALUE module)
{
VALUE p;
int changed = 0;
const st_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));

while (module) {
int superclass_seen = FALSE;

if (RCLASS_ORIGIN(module) != module)
goto skip;
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))
return -1;
/* ignore if the module included already in superclasses */
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {
switch (BUILTIN_TYPE(p)) {
case T_ICLASS:
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {
if (!superclass_seen) {
c = p; /* move insertion point */
}
goto skip;
}
break;
case T_CLASS:
superclass_seen = TRUE;
break;
}
}
c = RCLASS_SUPER(c) = rb_include_class_new(module, RCLASS_SUPER(c));
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {
VALUE refined_class =
rb_refinement_module_get_refined_class(klass);

st_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i,
(st_data_t) refined_class);
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);
}
if (RMODULE_M_TBL(module) && RMODULE_M_TBL(module)->num_entries)
changed = 1;
if (RMODULE_CONST_TBL(module) && RMODULE_CONST_TBL(module)->num_entries)
changed = 1;
skip:
module = RCLASS_SUPER(module);
}

return changed;
}

include_modules_at 函数做了许多工作,值得好好分析一下。

首先,最外层的 while 循环对 module 的祖先链(ancestors)逐一遍历。对于每个 module,内层的 for 循环会检查 module 是否已经包含在 klass 的祖先链中,如果已经包含,则跳过,否则,把 module 插入到 cc 的父类之间。

在测试 module 是否已包含在祖先链的过程中,变量 superclass_seen 用来判断是否越过了一个非包含类,如果是,则不移动插入点。所以模块包含不会将模块插到祖先链中下一个非包含类之后。

模块的插入工作是由第 31 行代码完成的,它调用 rb_include_class_new 函数为模块创建了一个包含类(include class),它以 c 的父类作为父类,接着又将这个包含类设置为 c 的父类。我们来看看 rb_include_class_new 函数是如何创建包含类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
VALUE
rb_include_class_new(VALUE module, VALUE super)
{
VALUE klass = class_alloc(T_ICLASS, rb_cClass);

if (BUILTIN_TYPE(module) == T_ICLASS) {
module = RBASIC(module)->klass;
}
if (!RCLASS_IV_TBL(module)) {
RCLASS_IV_TBL(module) = st_init_numtable();
}
if (!RCLASS_CONST_TBL(module)) {
RCLASS_CONST_TBL(module) = st_init_numtable();
}
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);
RCLASS_M_TBL(klass) = RCLASS_M_TBL(RCLASS_ORIGIN(module));
RCLASS_SUPER(klass) = super;
if (RB_TYPE_P(module, T_ICLASS)) {
RBASIC(klass)->klass = RBASIC(module)->klass;
}
else {
RBASIC(klass)->klass = module;
}
OBJ_INFECT(klass, module);
OBJ_INFECT(klass, super);

return (VALUE)klass;
}

第 4 行,class_alloc 创建了一个新类,并为它设置了 T_ICLASS 标识,有了这个标识,Ruby 就会认为它是一个包含类。接着就是一系列的初始化,将新类的内部表指向模块中对应的表。最后,将新类的 klass 指向模块,这样,包含类就创建完成了。

第 17 行有个 RCLASS_ORIGIN 宏,我们来看看它的作用是什么,Ruby 源码中,该宏的定义如下:

#define RCLASS_ORIGIN(c) (RCLASS_EXT(c)->origin)

可以看到,它指向一个类的 origin 成员。简单来说,它获取某个类的原始类。原始类跟模块前置有着密切的关系。关于模块前置的更多信息,请阅读《Ruby 中模块前置的实现》这篇文章。

了解这些信息之后,我们来总结一下。为了便于描述,我们以文章开头的那段代码作为例子,来回顾一下模块包含的全过程。

A = Module.new

B = Module.new

module C
include A, B # Inclusion is done by this line
end
  1. 当调用模块 Cinclude 方法时,默认实现 Module#include 会以相反的顺序对参数模块回调 append_featuresincluded 方法。真正的包含逻辑在 Module#append_features 的内部实现。
  2. 对每个被包含的模块,Ruby 会遍历其祖先链。对链上的每个模块,会首先检查是否已包含过,如果已包含,则跳过,否则为该模块创建一个包含类,插入到模块 C 的祖先链中。

当然,以上的描述省略了一些细节,比如类型安全检查、对插入点的调整、检查循环包含、以及对 Refinement 的处理。如果读者有兴趣,可以参照源代码去理解。

模块包含的本质是将被包含的模块的祖先链插入到另一个模块的祖先链中,其实最终,都是在维护一个祖先链。