很多人学 Ruby 时都听过"一切皆对象",于是自然以为字符串对象自带 upcase,用户对象自带 name。但打开 CRuby 的源码看一个实例,你会发现对象里根本没有方法。它只存实例变量、一些内部标志,以及一个指向类对象的指针。方法代码一行都不在对象里。

方法住在类里。定义一个类并写入实例方法时,Ruby 会把方法条目放进类内部的方法表,键是方法名符号,值是编译后的字节码。调用方法时,Ruby 先顺着接收者对象的类指针找到类,再在类的方法表里搜索。找到就执行,找不到就继续沿继承链向上。这样设计很省内存:十万个实例共享同一个类方法表,每个实例只保留自己的实例变量。
单例方法是个容易让人卡住的地方。你可以只给某一个对象加方法,不影响同类其他实例。既然对象不存方法,类也不能存这个方法,那它在哪里?Ruby 会临时创建一个隐藏的匿名类,通常叫 eigenclass 或 singleton class。它把对象的类指针重定向到这个单例类,把单例类的方法表里写入新方法,再把单例类的父类设回原来的类。调用时先进入单例类找到方法;调用普通方法时,单例类里没有,就沿父类指针回到原类。常规代码几乎看不见这个单例类,对象的 class 仍然返回原来的类,但 singleton_class 能把它暴露出来。

由此可以推翻一个常见说法:Ruby 里没有真正的"类方法"。写 def self.foo 或 class << self,本质是在类对象上定义单例方法。类本身也是对象,是 Class 的实例,所以它同样拥有自己的单例类。所谓类方法,不过是住在类对象的单例类里的实例方法。整个语言只遵循一条统一规则。

方法查找的顺序是确定的。接收者先看自己的单例类,再看单例类上 prepend 的模块,然后到接收者所属的类,接着是类上 prepend 的模块、include 的模块,再沿父类向上,依次检查每一层的 prepend、自身方法和 include,最后到达 Object、Kernel 和 BasicObject。第一个匹配到的方法立即执行。

如果整条链都没有找到,Ruby 不会马上报错。它会回到最初的接收者,从第一步重新搜索一遍,这次找的是 method_missing。因为 Kernel 提供了默认的 method_missing 并抛出 NoMethodError,所以只有没有任何中间层拦截时程序才会崩溃。这个两遍搜索机制支撑了许多元编程库,比如 ActiveRecord 的动态查找器和 OpenStruct。

"一切皆对象"还要面对性能问题:如果每个整数都分配堆内存,循环会慢得没法用。CRuby 用 tagged pointer 解决这件事。在 64 位系统上,堆地址按 8 字节对齐,最低三位总是 0。Ruby 把小的整数左移一位,再把最低位设为 1,这样虚拟机看到最低位是 1 就知道它不是堆指针,而是整数本身。true、false 和 nil 也各有固定的位模式。调用 1_000_000.times 时,并没有一百万个堆结构被分配,方法查找仍然走类的方法表,但省掉了内存分配和垃圾回收的开销。
理解这套模型后,元编程就不再像黑魔法。对象负责状态,类负责方法,单例方法藏在 eigenclass 里,类方法只是类对象的单例方法,方法查找沿 ancestors 链有序进行,未命中时还有 method_missing 兜底。这些机制彼此咬合,构成 Ruby 对象模型的基本骨架。
