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 依次检查:

  1. obj 的单例类:只挂在 obj 这一个对象上的方法;
  2. obj 的类:包括 prepend 进去的模块(模块在类之前);
  3. 类中 include 的模块:后 include 的在前;
  4. 父类:重复 2-3 的模式逐级向上;
  5. 整条链走完仍无 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 重载,正是为了保证缓存与代码一致。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台