Ruby 探秘模块混入 (include Module) 背后的故事

qinfanpeng · June 27, 2016 · Last by so_zengtao replied at August 26, 2016 · 6436 hits
Topic has been selected as the excellent topic by the admin.

原文地址:http://qinfanpeng.github.io/jekyll/update/2016/06/24/the_secret_of_module_include.html 对应视频版:http://v.youku.com/v_show/id_XMTYyMzAyOTY5Mg==.html

先上段代码:

class Person
  def wear
    '穿衣'
  end
end

class Economist < Person
end

economist = Economist.new
economist.wear # => ? ①

module Professor
  def wear
    "戴眼镜"
  end
end

class Economist < Person
  include Professor
end

economist.wear # => ?②

module Professor
  def wear
    "#{super},戴眼镜"
  end
end

economist.wear # => ?③

module Expert
  def refute_rumor
    '辟谣'
  end
end

module Professor
  include Expert
end

economist.refute_rumor # => ?④

Economist.include Professor
economist.refute_rumor # => ?⑤

相信对于上面四处问号,我们都有自己的答案了。毫无疑问①处会返回“穿衣”,这里我们都不会错;②是返回“穿衣”还是“戴眼镜”呢?估计要是没有③处的对比的话,估计有人会回答错误,②的正确答案是“戴眼镜”;那么③的正确答案自然是“穿衣,戴眼镜”了;④处会返回“辟谣”吗?,并不会,这里会报错:No Method Error;⑤会正确返回“辟谣”

不过前面答对与否,我们最好理解背后的原理,尤其是④处的情况。①处最好理解,调用了继承自Personwear方法而已,继承体系大致如下:

下面来看②③两处处,由前面的答案可知②调用的是来自Professormodule 中的wear方法,而非PersonClass 中的。结合②③两处,可大致得出如下继承体系:

等等,如果是这样的话,那么Professor被另一个 class include 的时候,该怎么办呢?总不能说一个 module 有多个 super 吧。记得以前看《Ruby 元编程》的时候,里面有很多类似的图。为了节约时间就不去翻书了,直接去 Google Image 里搜索关键字:ruby include,不难发现这张图:

“被 include 的实际是 module 的副本,而非 module 本身”,如此一来 module 被 include 无论多少次都无所谓了,因为更改的是那些对应副本的super。所以前面的图应该改成这样才对:

再看下③处,这里我们是在Professor被 include 后(实际上被 include 的是的Professor的副本),用类似打开类的方式修改的Professor,并没有修改其副本。这里正确返回“穿衣,戴眼镜”,能说明Professor和它的副本共享了同一份方法实现,即是说拷贝的时候并没拷贝底层的方法,类似下面这样:

与③不同是④是通过在Professorinclude 另外一个 moduleExpert来修改的,而这个过程又发生在Professor被 include 之后。这种情况下Economist感知不到Professor的改变。究其深层原因,结合前面的经验,不难得出以下结果:

我们索性一鼓作气画一下⑤处的情况:

小结

  1. 任何时候 include module 都 include 的是对应的副本,而非 module 本身。这是由于 include 会改变继承体系,如果直接 include module 本身的话,就使得该 module 难以再被其他地方 include 了,而每次都 include module 的副本则不会有这样的问题。
  2. module 和它被 include 的副本共享同一份方法实现,因而直接修改方法,会反映到已经 include 的地方去。
  3. 通 include 另一个 module B 的方式修改一个已经被 include 了的 module A,不会体现到 include A 的地方,因为 B 并没有被 include A 的副本中去。

参考资料

  1. 《Ruby Under a Microscope》(对应中文版《Ruby 原理剖析》由张汉东先生翻译,应该不久就可以和大家见面了。如果说《Ruby 元编程》让我们了解很多 Ruby 高级用法的话,那么《Ruby 原理剖析》则在更深层次上系统地阐述了这些高级用法背后的原理)
  2. 《Ruby 元编程(第 2 版)》

让我想到自己之前在处理一个问题 ( 传送门) 的时候也是没有用数据本身,而是用了数据 clone 出来的副本。但一直报错,逐一排查发现是没法 data = self.clone。最后一怒之下直接 data = self,然后对 data 进行操作。

不过感觉 data = self 和 data = self.clone 并没有太大的差别,两种赋值后 data 变量最终都是作为一个副本存在的。

但最后并没有深入探究到底为毛没法 self.clone,惭愧之.....学习了!

#1 楼 @catherine 😅 ,我也是带着问题去思考,收获多一些。

lz 图是用什么工具画的 看得我爱不释手~

jasl mark as excellent topic. 28 Jun 20:13

#3 楼 @mulderlover 用的是 Chrome 的离线应用(免费),貌似也有 web 版的。

樓主說得很清晰👏,《Ruby under a microscope》值得一讀

对于这个问题,《Effective Ruby》第六条有详细说明

ruby 的 module 比 python 可调戏性高到不知道哪里去了。。。。

#7 楼 @freefishz 是的,我也刚看过,结合 ruby 的 singleton class 也是很好理解的。

include 的 module 会变成一个匿名单列类,加入被 include 的继承体系中,与之对应的是 perpend

3 和 4 讲的很好 Economist 感知不到 Professor 的改变 会不会是在 append_features 之类的地方动了手脚

也就是说在 module 里面定义新的方法 和 通过 include Module 引入的方法有不同

You need to Sign in before reply, if you don't have an account, please Sign up first.