幾乎隨便問一位開發者物件導向程式設計是怎麼運作的,他們都會描述出同一張教科書式的圖像:一個物件把資料和方法打包成一個單一單元。
在 Ruby 裡,你也會不斷聽到這個觀念以口號的形式被強調:「萬物皆物件。」
你建立一個字串,對它呼叫 .upcase,很自然地就會以為這個字串物件擁有那個方法。你建立一個使用者物件,呼叫 .save,也會假設 .save 就存在於你的 user 裡。
但 Ruby 實際上完全不是這樣運作的。
在 Ruby 中,物件不包含任何方法。
如果你從 CRuby(MRI)的 C 原始碼層級去檢視一個實例,你會發現一些令人驚訝的事:物件只儲存它的實例變數、一組內部旗標,以及一個指向另一個結構的指標。它不包含任何一行方法程式碼,也不包含方法表。
那麼方法到底存在於哪裡?Ruby 又是怎麼知道當你輸入 user.greet 時要執行什麼?而當你只在單一特定物件上定義方法時,實際上又發生了什麼事?
若要真正掌握 Ruby,你必須撥開語法糖,理解 Ruby 物件模型的機制:方法表、隱藏的 eigenclass,以及祖先鏈。
讓我們看一個簡單的 Ruby 類別:
class User
def initialize(name, email)
@name = name
@email = email
end
def greet
"Hello, I am #{@name}"
end
end
user = User.new("Alice", "[email protected]")
當 Ruby 求值 User.new 時,虛擬機器會配置一個稱為 RObject 的堆積結構。
在這個 RObject 裡,Ruby 會儲存兩件事:
@name 被設成 "Alice",@email 被設成 "[email protected]"。klass 的內部指標,直接指向 User 類別物件。請注意 user 裡缺少了什麼:找不到 greet 方法。

Ruby 的方法只存在於類別之中(內部表示為 RClass)。
User 類別包含一個稱為 m_tbl 的雜湊表(方法表)。當你定義 def greet 時,Ruby 會在 User 的方法表中寫入一筆專案,把符號 :greet 對應到它編譯後的 bytecode 本體。
當你呼叫 user.greet 時:
user 內部找。user 的內部 klass 指標前往 User 類別。User 的 m_tbl 中搜尋 :greet。self = user 的上下文中執行它。這種分離設計讓 Ruby 更有效率。你可以建立十萬個 User 物件,而這十萬個物件都共享 User 裡同一份方法表。實例只會持有各自的實例變數。
現在來看 Ruby 一個很有名的特性:你可以只替某個單獨物件新增方法,而不影響同一類別的其他實例。
str1 = "hello"
str2 = "world"
def str1.shout
upcase + "!!!"
end
str1.shout #=> "HELLO!!!"
str2.shout #=> NoMethodError: undefined method 'shout' for "world":String
這引出一個有趣的謎題:
:shout 不可能存在於 str1 裡。:shout 也不可能存在於 String 裡,否則 str2 也會能用到它。如果物件不能持有方法,而類別也不能持有這個方法,那 shout 到底藏在哪裡?
Ruby 用一個非常巧妙的方式解決:它會悄悄即時創造一個看不見的類別。

當你在 str1 上定義方法的瞬間,Ruby 會建立一個匿名、隱藏的類別,稱為 eigenclass(正式名稱是 singleton class,單例類別)。
接著 Ruby 會做一個架構上的偷天換日:
str1 的內部 klass 指標,並把它重新指向這個新的單例類別(#<Class:str1>)。String。:shout 方法放進這個單例類別的方法表中。當你呼叫 str1.shout 時,Ruby 會沿著 klass 指標進入 #<Class:str1>,找到 :shout 並執行它。
當你呼叫 str1.upcase 時,Ruby 會先在 #<Class:str1> 中查找,找不到 :upcase 後,沿著 superclass 指標往上走到 String,找到 :upcase 後執行它。
對一般程式碼來說,單例類別幾乎是完全透明的。你在 str1.class 裡看不到它(仍然會回報 String)。但你可以透過 Ruby 的反射方法證明它存在:
str1.singleton_class
#=> #<Class:#<String:0x00007fffa80c2180>>
str1.singleton_methods
#=> [:shout]
一旦理解了 eigenclass,Ruby 裡最大的迷思之一就會消失:根本沒有什麼「類別方法」這回事。
開發者常常會寫出像這樣的程式:
class Post
def self.recent
# 查詢最近的文章
end
end
教學文章常常會說:「這是在 Post 上定義一個類別方法。」
在 Java 或 C++ 之類的語言裡,靜態方法是獨立的語言結構。但在 Ruby 中,Post 並不是什麼特殊實體。Post 只是一個物件。 它是 Class 類別的實例。
Post.class
#=> Class
因為 Post 是物件,所以寫 def Post.recent 或 def self.recent,跟寫 def str1.shout 做的是完全同一件事:你是在為一個物件定義單例方法。

:recent 方法既不住在 Post 裡,也不住在 Class 裡。它存在於 Post 自己私有的 eigenclass(#<Class:Post>)中。
在 Ruby 裡,只有實例方法。所謂的「類別方法」,其實只是存在於類別物件的單例類別上的實例方法。整個語言都遵循同一條統一規則運作。
當你輸入 receiver.message 時,Ruby 查找程式碼的精確順序是什麼?
Ruby 不會漫無目的地搜尋。它會沿著一條精確且可預測的階梯前進,這條階梯稱為 ancestors chain(祖先鏈)。它遇到的第一個匹配會立刻執行。

以下是完整的逐步查找路徑:
prepend 加入的模組都會排在 eigenclass 前面。prepend 到類別上,它的方法會覆蓋類別本身的方法。include 到類別中,它會直接插入在階層中、位於類別之上。如果 Ruby 一路爬到 BasicObject 都找不到對應的方法,它不會立刻丟出錯誤。
相反地,Ruby 會回到起始接收者,從步驟一開始整個搜尋流程再跑一次,這次尋找的是名為 method_missing 的方法。
因為 Kernel 提供了一個預設的 method_missing 實作,會拋出 NoMethodError,所以只有在沒有任何中途模組或類別攔截它時,你的程式才會崩潰。
這個雙重搜尋流程正是 Ruby 著名的 metaprogramming 函式庫背後的動力來源,從 ActiveRecord 的動態查找器到 OpenStruct 都是如此。
如果 Ruby 裡每個數字、布林值與 nil 都是具有方法的物件,那 Ruby 又是怎麼避免為基本運算分配堆積記憶體而拖垮效能的?
如果你執行:
total = 0
1_000_000.times { |i| total += i }
Ruby 會在堆積上配置一百萬個 RObject 結構嗎?
在 CRuby 中,答案是不會,這要歸功於一個內部最佳化技術,稱為 Tagged Pointers(標記指標;也常稱為 Immediate Values,立即值)。

在 64 位元作業系統上,記憶體位址會對齊到 8 位元組邊界。這表示任何有效堆積指標的最低 3 個 bit 永遠是 000。
CRuby 利用這些未使用的 bit。當一個 64 位元值代表一個小整數(稱為 Fixnum)時,Ruby 會把整數左移一個 bit,並把最低位設為 1:
value = (integer << 1) | 1
當 Ruby 虛擬機器看到某個值的最低位是 1 時,它就知道:「這不是堆積指標,這就是原始整數本身。」
同樣地,CRuby 也為單例值保留了特定的 bit 模式:
false 表示為 0x00nil 表示為 0x08true 表示為 0x14當你呼叫 42.even? 時,Ruby 不會去檢查某個堆積區塊。它會偵測到這個 tagged pointer,去 Integer 類別的方法表查找方法,並在沒有任何記憶體配置或垃圾回收負擔的情況下直接執行程式碼。
你同時得到了「萬物皆物件」這種概念上的純粹性,以及原始暫存器算術的效率。
Ruby 的物件模型常被描述得很複雜,但在表面底下,它其實非常精簡:
klass 指標。 它們永遠不會持有方法。super 指標。 它們永遠不會持有實例狀態。一旦你用這個角度看待物件模型,Ruby 的 metaprogramming 就不再像黑魔法。像 class_eval、instance_eval、prepend 和 singleton class 這些概念,都會變成可預測的機械化工具,遵循同一套一致的規則。
你第一次學 Ruby 時,最讓你停下來重新思考程式語言運作方式的物件模型怪癖是什麼?如果你曾在正式環境中使用過 eigenclass,你是用它解決了什麼問題?
參考資料:
原文出處:https://dev.to/smtahosin/in-ruby-objects-dont-have-methods-how-method-lookup-actually-works-31g9