Ruby 2.5.0中如何解决菱形Mixin的方法查找问题?
嘿,这个问题问得特别到位!Ruby处理菱形混入(也就是同一个模块通过多个路径被引入到类中)的核心,全靠祖先链(ancestors chain)的线性唯一性和单向方法查找规则来解决,咱们结合你给出的例子一步步拆解:
先看你的示例代码和结果
module M3; end module M1; prepend M3 end module M2; prepend M3 end class Base include M1 include M2 end p Base.ancestors # => [Base, M3, M2, M1, Object, Kernel, BasicObject]
这个结果看起来有点反直觉,但其实是Ruby祖先链构建规则的必然产物,咱们拆解背后的逻辑:
1. 模块嵌套的prepend先构建子链
当你执行M1 prepend M3时,相当于给M1的方法查找路径“加了个前置过滤器”——调用M1的任何方法,都会先去M3里找,找不到再回M1本身。这时候M1的内部祖先链是[M3, M1];同理,M2 prepend M3后,M2的内部祖先链是[M3, M2]。
2. 类include模块时的链合并规则
Ruby在处理类的模块引入时,会严格维护祖先链的无重复线性结构,同时遵循“最后引入的模块优先级更高”的原则:
- 当Base第一次
include M1时,Ruby会把M1的完整子链[M3, M1]插入到Base和Object之间,此时Base的祖先链是:[Base, M3, M1, Object, Kernel, BasicObject] - 当Base接着
include M2时,Ruby会先检查M2的子链[M3, M2]:发现M3已经在当前祖先链里了,就不会重复添加;然后把M2插入到最靠近Base的位置(M3之后、M1之前),这样保证最后引入的M2优先级比M1高。最终就得到了你看到的结果。
3. 方法查找的核心逻辑
Ruby的方法查找是沿着祖先链从前往后单向遍历的:从Base本身开始,依次检查M3、M2、M1……直到找到第一个匹配的方法就停止。
比如如果M3、M2、M1都定义了同一个方法foo,那么调用Base.new.foo时,会执行M3里的foo;如果M3没定义,就会执行M2的foo,以此类推。
再说说把M2的include改成prepend的情况
如果把代码改成:
class Base include M1 prepend M2 end
此时Base的祖先链会变成:[M3, M2, Base, M1, Object, Kernel, BasicObject]
这是因为prepend会把模块的子链直接放到类本身的前面,优先级比类自己的方法还高。此时方法查找会先找M3,再M2,再Base本身,最后才是M1。
总结:Ruby如何解决菱形歧义
Ruby没有用复杂的多路径判断,而是通过将所有模块依赖线性化为唯一的祖先链,让方法查找变成简单的单向遍历。不管模块被多少个路径引入,最终在类的祖先链里只会出现一次,从根本上避免了菱形混入中“同一个模块被多次查找”的歧义问题。
内容的提问来源于stack exchange,提问作者Kenta Nakajima

