Ruby 对象模型与方法查找链(Method Lookup)详解
一切皆为对象的 Ruby 如何找到你要调的方法?本文图解方法查找链:单例方法→本类→include 的模块→父类,讲解 eigenclass、prepend 的影响,以及用 ancestors 自省的技巧。
Ruby 调用方法时沿固定链条查找:对象单例类(eigenclass)→ 本类及 prepend 模块 → include 的模块(后 include 优先)→ 父类,逐级向上直到 BasicObject;都找不到就触发 method_missing。类.ancestors 可以把这条链完整打印出来。
一切皆对象,对象皆有类
Ruby 的一致性哲学:1 是 Integer 的实例,Integer 是 Class 的实例,Class 又继承自 Object。类本身也是对象,所以类也有类——理解这句话,方法查找就只剩下"沿链走"一个动作。
方法查找的标准路径
调用 obj.foo 时,Ruby 依次检查:
- obj 的单例类:只挂在 obj 这一个对象上的方法;
- obj 的类:包括 prepend 进去的模块(模块在类之前);
- 类中 include 的模块:后 include 的在前;
- 父类:重复 2-3 的模式逐级向上;
- 整条链走完仍无
foo→ 调用method_missing。
module Loggable; end
class Animal
def speak; '...'; end
end
class Dog < Animal
prepend Loggable
def speak; 'Woof'; end
end
Dog.ancestors
# => [Loggable, Dog, Animal, Object, Kernel, BasicObject]
ancestors 输出就是查找顺序的真实写照。
eigenclass:每个对象的私有类
给单个对象定义方法:
dog = Dog.new
def dog.fetch
'叼回来'
end
fetch 不在 Dog 类里,而在 dog 的单例类(eigenclass,也称 singleton class)上——这是 Ruby 为每个对象隐式维护的特殊类,插在查找链最前端。用 singleton_class 可以拿到它,dog.singleton_methods 列出所有单例方法。
类方法本质上也是单例方法:def Dog.bark 就是定义在 Dog 这个类对象的 eigenclass 上——extend 模块的效果同理,模块被插进类的 eigenclass 祖先链。
include 与 prepend 在链上的位置
class C
include M1
include M2
prepend M0
end
C.ancestors # => [M0, C, M2, M1, Object, ...]
prepend 的模块在类前面(可覆盖类方法并 super 回调),include 的模块在类后面、后 include 的更靠前。这条规则解释了为什么同名方法的覆盖行为有时"反直觉"。
自省:用代码探索对象模型
obj.class # 对象的类
obj.singleton_methods # 单例方法列表
Dog.instance_method(:speak) # 拿到方法对象本身
Dog.instance_method(:speak).owner # 方法定义在哪个模块/类
obj.method(:speak).source_location # 源码位置
排查"这个方法到底哪来的"这类问题时,owner 和 source_location 是最快的两把刀。
常见问题(FAQ)
Q:super 是按什么规则找父实现的?
super 沿 ancestors 链找当前方法定义位置的下一个同名方法——不是简单的"父类方法"。模块里写 super 时,找到的是链上排在模块后面的实现,这正是 prepend + super 能叠加增强的原理。
Q:BasicObject 和 Object 有什么区别?
BasicObject 是查找链的尽头,只有 ==、!、__send__ 等极少方法,连 puts、class 都没有。需要"白板对象"(如代理类、DSL 容器)时继承 BasicObject,避免方法名冲突;普通类都继承 Object。
Q:方法缓存会让动态修改失效吗?
Ruby 会缓存方法查找结果,定义/删除方法、include 模块会使相关缓存失效(这是生产环境慎用在运行时改类的原因之一)。开发期改代码要重启或依赖 Zeitwerk 重载,正是为了保证缓存与代码一致。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。