您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
Ruby 2.0 中模块前置的实现
 
作者 言无不尽,火龙果软件    发布于 2014-07-15
   次浏览      
 

Ruby 2.0 为模块添加了一个 Module#prepend 方法,根据 API 文档的描述,它以相反的顺序对参数逐一调用 prepend_features 方法。和模块包含类似,会把一个模块的祖先链插入到另一个模块的祖先链中,但跟模块包含把祖先链插到该模块之后不一样,它会把祖先链插到该模块之前。我喜欢把这个特性叫做模块前置。

先举个例子来说明模块前置的作用,并和模块包含作了对比。在这个例子中,类 C 前置了模块 A 和 B;类 D 包含了模块 A 和 B。

module A
def foo; 'A' end
end
module B
def foo; 'B' end
end
class C
prepend A, B # Prepending is done by this line
def foo; 'C' end
end
class D
include A, B
def foo; 'D' end
end
C.ancestors # => [A, B, C, Object, Kernel, BasicObject]
D.ancestors # => [D, A, B, Object, Kernel, BasicObject]
C.new.foo # => 'A'
D.new.foo # => 'D'

第 10 行,我们在 C 中前置了模块 A 和 B,这样 A 和 B 就插入到 C 的祖先链中了。从 21 和 22 行中的注释中,我们可以看到,在 C 的祖先链中,A 和 B 位于 C 之前。而在 D 的祖先链中,A 和 B 位于 D 之后。

这就是为什么第 24 行,C.new.foo 的返回值是 'A',因为模块 A 前置于 C,位于祖先链的最前面,方法查找会优先找到 A 中的 foo 方法。

我们看到了模块前置的强大的特性,但疑问也随之而来。最显而易见的问题是,C.ancestors 为什么不是从类 C 开始。要解开这个疑问,首先应该弄清楚 prepend 方法都做了哪些工作。我们跟到源代码中去一探究竟,下面是 prepend 的默认实现 Module#prepend 对应的源代码:

static VALUE
rb_mod_prepend(int argc, VALUE *argv, VALUE module)
{
int i;
ID id_prepend_features, id_prepended;
CONST_ID(id_prepend_features, "prepend_features");
CONST_ID(id_prepended, "prepended");
for (i = 0; i < argc; i++)
Check_Type(argv[i], T_MODULE);
while (argc--) {
rb_funcall(argv[argc], id_prepend_features, 1, module);
rb_funcall(argv[argc], id_prepended, 1, module);
}
return module;
}

可以看到,它的行为和 Module#include 方法几乎一样,只不过回调的方法不一样。这里,它回调了参数模块的 prepend_features 方法和 prepended 方法。同样,Module#prepend_features 才是真正干活的地方,所以跟进去看看。

static VALUE
rb_mod_prepend_features(VALUE module, VALUE prepend)
{
switch (TYPE(prepend)) {
case T_CLASS:
case T_MODULE:
break;
default:
Check_Type(prepend, T_CLASS);
break;
}
rb_prepend_module(prepend, module);
return module;
}
  它做了一些类型方面的检查, 然后把工作交给了 rb_prepend_module 函数,我们看看 rb_prepend_module 函数做了什么。
void
rb_prepend_module(VALUE klass, VALUE module)
{
void rb_vm_check_redefinition_by_prepend(VALUE klass);
VALUE origin;
int changed = 0;
rb_frozen_class_p(klass);
if (!OBJ_UNTRUSTED(klass)) {
rb_secure(4);
}
Check_Type(module, T_MODULE);
OBJ_INFECT(klass, module);
origin = RCLASS_ORIGIN(klass);
if (origin == klass) {
origin = class_alloc(T_ICLASS, klass);
RCLASS_SUPER(origin) = RCLASS_SUPER(klass);
RCLASS_SUPER(klass) = origin;
RCLASS_ORIGIN(klass) = origin;
RCLASS_M_TBL(origin) = RCLASS_M_TBL(klass);
RCLASS_M_TBL(klass) = st_init_numtable();
st_foreach(RCLASS_M_TBL(origin), move_refined_method,
(st_data_t) RCLASS_M_TBL(klass));
}
changed = include_modules_at(klass, klass, module);
if (changed < 0)
rb_raise(rb_eArgError, "cyclic prepend detected");
if (changed) {
rb_clear_cache();
rb_vm_check_redefinition_by_prepend(klass);
}
}

这个函数做了一些工作,我们来分析一下。前 16 行都是在做一些类型检查等工作,我们跳过。从第 17 行开始分析。

首先,宏 RCLASS_ORIGIN 获取 klass 的 origin 成员,并且把它和 klass 比较。我们不知道 origin 字段有什么作用,我们先假设测试条件为真,即 klass 的 origin 成员指向自身。我们来分析一下 if 语句中的逻辑:

19 行为 klass 创建了一个新的包含类,我们把它称为原始类;

20 ~ 21 行把新创建的包含类插入到 klass 和 klass 的父类中间;

22 行将 klass 的 origin 成员指向了新类;

接下来,23 ~ 24 行把 klass 的方法表转移到新类中,并清空 klass 的方法表;

最后,25 行又把 klass 原先的方法表中的 Refined 方法移了回来。

分析完 if 语句,我们继续前进,来到第 28 行。等等,你好像看到了熟悉的东西。没错,那就是 include_modules_at 方法。在前一篇文章中,我们讨论了这个函数,它用来包含某个模块。你简直不敢相信自己的眼睛,明明是在前置模块,怎么突然又变成包含模块了?

是的,没错,它就是在包含模块。被包含的模块的祖先链插入到了 klass 和 klass 的原始类之间。由于 klass 内部的方法表已经转移到上游的原始类中,所以插入的位置正好合适。Ruby 通过这种变换,巧妙地将前置模块转化为包含模块,太棒了。

下面这个图描述了文章开头的那个例子中,类 C 中 prepend A, B 语句执行前后的状态:

 +-----+      +--------+
  Before: | C |----->| Object |
  +-----+ +--------+
  +--------------- klass ----------------+
  | |
  v |
  +-----+ +-----+ +-----+ +-----+ +--------+
  After: | C |----->| A |----->| B |----->| C' |+---->| Object |
  +-----+ +-----+ +-----+ +-----+ +--------+
  | ^
  | |
  +--------------- origin ---------------+

正如之前分析的那样,C' 就是那个新创建的包含类,它是 C 的原始类。但如果是这样的话,还是无法解释之前的疑问:为什么 C.ancestors 不是从类 C 开始?要搞清楚这个问题,我们来看看 C.ancestors 是如何工作的。

我们找到了 Module#ancestors 的源代码,它看起来比较简单:

VALUE
rb_mod_ancestors(VALUE mod)
{
VALUE p, ary = rb_ary_new();
for (p = mod; p; p = RCLASS_SUPER(p)) {
if (FL_TEST(p, FL_SINGLETON))
continue;
if (BUILTIN_TYPE(p) == T_ICLASS) {
rb_ary_push(ary, RBASIC(p)->klass);
}
else if (p == RCLASS_ORIGIN(p)) {
rb_ary_push(ary, p);
}
}
return ary;
}

该函数首先创建了一个数组,然后对遍历模块的祖先链,对每个祖先,如果是包含类或者原始类指向自身,就放在返回的数组里面。另外,它还会跳过单例类。

至此,之前的疑问也得到了解释,C.ancestors 并没有把 C 自身包含进去,因为它既不是包含类,也不是 origin 指向自身的类。而 C.ancestors 返回的数组中的 C 其实是 C 的原始类,同时也是 C 的包含类,所以它才有 C 这个名字。

模块前置其实也是通过模块包含来完成的,只不过在包含之前做了一些特殊处理:创建了一个原始类,然后在原始类之前包含模块。但由于原始类也是一个包含类,因此被前置模块的某个祖先可能会越过原始类,比如下面这个例子:

A = Module.new
module B
def bar; 'B' end
end
module C
include A, B
end
class D
include A
prepend C
def bar; 'D' end
end
D.new.bar # => 'D'

类 D 在前置模块 C 之前,包含了模块 A。下图中,前两个是 D 前置模块 C 之前,D 和 C 的祖先链,第三个是 D 前置模块 C 之后,D 的祖先链。

+---+    +---+    +--------+
  | D |--->| A |--->| Object |
  +---+ +---+ +--------+
  +---+ +---+ +---+ +--------+
  | C |--->| A |--->| B |--->| Object |
  +---+ +---+ +---+ +--------+
  |
  +-----------------+ A 使得插入点移到了 D' 的后面
  |
  v
  +---+ +---+ +---+ +---+ +---+ +--------+
  | D |--->| C |--->| D'|--->| A |--->| B |+-->| Object |
  +---+ +---+ +---+ +---+ +---+ +--------+

正如图中标注的那样,模块 A 的存在使得插入点移到了 D' 的后面,所以 B 位于 D' 的后面。所以 D.new.bar 的值是 'D' 而不是 'B'。

现在,我们理解了模块前置在 Ruby 内部是如何实现的。模块前置非常有用,只要明白了它是如何工作的,你一定能想到它的用武之地。

   
次浏览       
相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...