Ruby是一門單一繼承的面向對象語言,那么在內部結構上,它是以object為根節點的樹形結構的類圖,那么我們在Ruby中定義的方法和常量也,依附在這個結構之上的,那么方法和常量是如何被定為查找的呢,我們要從模塊說起。
模塊
類在Ruby的內部結構中,是使用RClass結構體來表示的,但是模塊不是使用類似,RModule這種結構實現的,而是同樣的使用了RClass,這樣的結構看起來非常的簡潔,在內部保持了一致性,為方法和常量查找算法提供了簡便的基礎。
模塊在內部同樣使用了 RClass和rb_classext_struct 兩個結構體,所以說模塊在某種程度上說也是類的一種,我們看到上圖的結構體中,較以往的RClass結構略有不同,因為模塊不需要實例化,所以也就去掉了一下與實例相關的結構,可以說Ruby的模塊就是包含方法定義,常磊指針和常量表的Ruby對象。
那么模塊在內部是怎么被添加到類中的呢。
module Professor
end
class Mathematican < Person
include Professor
end
Ruby是在模塊Professor的RClass的基礎上做了一個副本,然后將這個副本作為Mathematican類的超類,這種形式將Professor模塊添加到了Mathematican類的繼承鏈上。

Ruby使用了同樣的方式實現,模塊的extend,只不過被修改的繼承鏈換成了,目標類的元類(metaclass)上了

方法查找
方法調用,其實就是給被調用的方法的類發消息,那么具體想哪一個類發消息就是關鍵的問題了,方法查找算法就是,確定方法的定義是存在于哪一個類的上下文當中,在Ruby中因為整個的類結構是屬性的,包括繼承鏈中的模塊也是按照繼承的方式被添加到其中的,所以方法查找算法子Ruby是非常巧妙的簡單。
從上圖我們就可以看出來,方法查找的整個過程是一目了然的簡單,但是如果每次的方法調用都要經過,整個樹形結構的遍歷的話,效率不是很好,所以Ruby在方法查找的功能中添加了全局方法緩存和內聯方法緩存這兩個緩存,來保證方法查找的效率。
- 全局方法緩存,是用于保存接收者和實現類之間的映射表,Ruby在第一次方法查找之后就會將查找鏈路添加的映射表中,當第二次查找該方法的時候就可以直接使用映射表中的目標類了。
-
內聯方法緩存,是將方法執行的YARV指令直接進行緩存的工具,這樣在方法的第二次查找是可以直接的執行YARV指令,進一步提升速度。
有緩存就有緩存的失效機制,兩個方法查找緩存,都是在Ruby創建和清除方法或者是include模塊的時候進行緩存清除的。
Prepend
類中引入兩個模塊的時候,Ruby的方法查找是按照模塊引入的順序進行查找的,后引入的模塊會在,繼承鏈的倒數第二的位置上。那么Ruby模塊的prepend方法的查找又是如果進行的呢,下面的代碼中 模塊和類都定義了name 方法,那么最后方法調用的時候,調用的會是Mathematician類屬性構造器定義的name方法。
module Professor
def name
"Prof. #{super}"
end
end
class Mathematician
attr_accessor :name
include Professor
end
m = Mathematician.new
m.name = 'Henri'
p m.name #=> Henri
如果我們想要讓Professor模塊中的方法重載類中的同名方法,就需要使用prepend修改一個例子了。
module Professor
def name
"Prof. #{super}"
end
end
class Mathematician
attr_accessor :name
prepend Professor
end
m = Mathematician.new
m.name = 'Henri'
p m.name #=> Prof. Henri
那么prepend是如何做到重載類中的方法的呢,其實秘密就是Ruby內部使用了一個小技巧,在使用prepend時,Ruby會在內部的創建目標類的副本(在內部叫原生類 origin class) 并且把它設置成前置模塊的超類,Ruby使用了rb_classext_struct結構體中的origin指針來記錄該類的原生副本,這樣在方法查找的時候,就會先找到prepend模塊的方法。

共享方法表
上面已經說過了,Ruby的模塊是通過將副本作為類的超類來進行繼承鏈方法查找的,那么如果我們事后修改了,模塊的方法定義的話,模塊的副本是不是引用的還是舊的方法定義呢,答案是否定的,Ruby在創建模塊的副本的時候并沒有一并負責模塊的方法表,而是讓副本和模塊的指針共同引用同一個方法表,所以當方法的定義被修改后,引入模塊的類還是會調用新的方法。
常量查找
在Ruby中常量不僅僅用于表示不可變值,它還是Ruby類和模塊的引用對象,也就是類和模塊的名字都是常量,那么常量查找的其實就是查找類和模塊。
常量本身是存放在RClass 結構體的constants常量表中的,普通的常量查找是通過和方法查找同樣的方式進行的,首先是先在本類的常量表中查找常量,如果沒有找到的話在到父類的常量表中查找。
class MyClass
SOME_CONSTANT = "Some value..."
end
class Subclass < MyClass
p SOME_CONSTANT
end
詞法作用域
上面說到了,Ruby是如何在本類和祖先類中查找常量的,但是在模塊實際的使用當中,模塊的命名空間常量查找又是如何進行的呢,這里就要提到Ruby在父級空間查找常量的詞法作用域問題了。
Ruby的詞法作用域,有作用域門控制,也就是 module 或者 class 這樣的關鍵字定義的作用域,還有就是誠信的默認『頂級作用域』在不同的作用域中為了定位程序代碼的位置,需要使用一對指針來對應作用域內的YARV指令片段。
- nd_next 指針,被設置為父層或上下文的詞法作用域。
- nd_class 指針,表示Ruby類或模塊對應的作用域。
有了上面的作用域結構,常量查找的算法也就變得簡單了。
我們上面說到了,Ruby常量查找的兩種方式,但是在真實的常量查找中是先使用哪種方式呢,簡單的說Ruby或先使用詞法作用域查找常量,如果沒有找到的話再使用超類鏈查找常量,注意這里的詞法作用域查找在真實的使用場景下,不僅僅是上圖所示,它還會在父詞法作用域中查找autoload關鍵字。