Chapter 2: The Object Model - Part II
” Whenever you get stuck if you are doing meta programming, and you get stuck trying to workout how come that’s nil, I guarantee you that if you track the value of self, you’ll understand it “ ― Dave Thomas ―
Self In Ruby’s World
Self 在 Ruby 的世界裡可以被理解為 "當下執行的物件(current object)",但是如何才能看出(current object)為何呢?在多數的情況下,當呼叫方法的時候,訊息接收者(receiver )會變成 self ,因此通常可從最後呼叫的方法,來找到 self。
以上面程式碼為例: 當我們呼叫 speak() 方法的時候,『訊息的接收者』為 cat 這個實體物件,也就是當下執行的物件 self。
@age 是 cat 的實體變數,因此在行數 4 可以看到 @age 會顯示數字 5。 接者執行 count() 此時,self 維持不變,也就是說 @age ㄧ樣是 cat 的實體變數。最後找到 count() 執行第10行後,回傳數字 6 在第 5 行。
Self 在以下兩種例外情況則需要特別地注意:
1. The Top Level
此物件當下是在 ruby 執行環境下的最頂層,有可能是還沒有開始呼叫 method 或者是所有的 method 都已經執行完成。 參考自 Metaprogramming Ruby
2. Class / Module Definition and self
當 self 在類別(class)或模組(module)時,self 會依所在的位置不同而改變。 以上面的程式碼來說,雖然第3行的 @size 是已經定值為 3,我們從第2行可得知這裡的 self 是 Animal,因此不難看出第3行的 @size 也是屬於 Animal class。 在第 8 行的 @size 會列出 nil ,其原因就是當 self 走到第8行時,self 是 cat 這個物件,而 speak() 方法內的 @age 和 @size 都是 cat 的 instance variables,因為只有 @age 被賦值 5, 因此 @size 會是 nil。
What Happens When You Call a Methods?
在 Metaprogramming Ruby 書中,當程式呼叫方法時,Ruby會做 2 件事情:
- Ruby 會依 “ 查詢方法流程 (method lookup)” 尋找被指定的方法
- Ruby 以當下物件本身 (self)去執行該方法
乍看之下似乎很簡單,但卻有不少的幕後工作是默默地執行。
查詢方法流程
要介紹 method lookup 之前,先需要了解什麼是 “receiver(接收者)” 以及 “ancestors chain” 。
Receiver
Receiver 就是當你呼叫方法時的接收者,例如:在先前提過的 obj.my_name(‘kevin’) ,obj 就是 my_name()方法的 receiver。
Ancestors chain
Ancestors chain 或許可以想像成是家族的族譜,意思是從找到 ‘接收者’ 的 class 後,開始持續往上找父層,ㄧ直到最上層 (BassicObject) 為止。 來看以下的圖會更清楚:
class Animal
def run
@str = "running"
end
end
class Cat < Animal
def speak
"meow"
end
end
kitty = Cat.new
kitty.run # => "running"
kitty.instance_variables # => [:@str]
kitty.class.instance_methods(false) # => [:speak]
kitty.class.instance_methods(true) # => [:speak, :run, :to_yaml...]
Method Lookup 流程解說
『 黃金法則:往右ㄧ步,再往上 』 "One step to the right, then up" 當 run() 方法被呼叫時,會先去找 run() 的接受者(receiver),也就是 kitty 物件(object)。接者會依著ㄧ個指向屬於CLASS的連結,往右找尋到 Cat 類別(Class),但是 Cat 類別裡沒有 run() 方法,因此再繼續往上層找,直到 run() 方法被找到為止。
Cat.ancestors # => [Cat, Animal, Object, Kernel, BasicObject]
在詢問 Cat 類別的繼承關係時,可以看出回傳陣列裡明明就有 Kernel,為什麼在 Method Lookup Diagram 中卻沒有畫出來呢?
What is Kernel?
Kernel 在 Ruby 程式語言裡,被 included 在 Object 類別內的模組(Module),因此每一個 Ruby object 都有 Kernel 模組的實例方法。例如:我們常見的 puts 方法就是來自 Kernel 模組的私有實例方法。
Kernel.private_instance_methods.grep(/^puts/) # => [:puts]
同樣地,我們可以利用開放類別的技巧覆寫或是新增方法,給所有的 Ruby 物件。
module Kernel
def puts(*args)
"Wrong puts, sorry"
end
end
puts "abc" # => "Wrong puts, sorry"
Method Lookup with modules
在能進一步討論 Method Lookup 之前,首先要認識三種模組常用的方法:include、prepend 和 extend:
include
module Kung_Fu
def kick
"Knockout Kick"
end
end
class Master
include Kung_Fu
end
class Disciple < Master
end
以繼承關係圖來看,當師傅(Master)類別 include 功夫(Kung_Fu)模組進來時,Ruby 會插入 Kung_Fu 模組在 Master 類別之上,並且 Kung_Fu 模組內的方法都會變成是 Master class 的 instance method
Disciple.ancestors # => [Disciple, Master, Kung_Fu, Object, Kernel, BasicObject]s
prepend
從 Ruby 2.0 開始,除了 include 方法之外,也可用 prepend 方法引入 module,並會放在下方。
module Boxing
def punch
"Knockout Punch"
end
end
class Coach
prepend Boxing
end
class Trainee < Coach
end
Trainee.ancestors # => [Trainee, Boxing, Coach, Object, Kernel, BasicObject]
extend
目前為止不管是 include 還是 prepend 都是從模組裡引入『實例方法』,但是如果要引入『類別方法』就需要用 Object#extend 方法來導入:
module Boxing
def punch
"Knockout Punch"
end
end
class Coach
extend Boxing
end
class Trainee < Coach
end
Coach.punch # => "Knockout Punch"
Coach.singleton_methods # => [:punch]
Trainee.ancestors # => [Trainee, Coach, Object, Kernel, BasicObject]
進階補充
補充的內容會需要先了解 Chapter 3 裡的 Singleton Method,所以晚點再回來看進階的部分會較適合。
在上面的程式碼最後一行所回傳的值似乎有點奇怪,竟然沒有 Boxing 的蹤影。
Trainee.ancestors # => [Trainee, Coach, Object, Kernel, BasicObject]
如果你已經看完了全部 Chapter 3,你或許就會猜到其實是 Ruby 把 Boxing 藏在 singleton class 裡了,當我們用 extend 將 Boxing 模組引入到 Coach 類別裡,實際上是打開 Coach 的 singleton class 並且 include 引入了 Boxing 模組。
我們可以從以下程式碼來驗證:
module Boxing
def punch
"Knockout Punch"
end
end
class Coach
class << self
include Boxing
end
end
Coach.singleton_methods # => [:punch]
Trainee.singleton_methods # => [:punch]
Coach.punch # => "Knockout Punch"
Trainee.ancestors # => [Trainee, Coach, Object, Kernel, BasicObject]
Multiple Inclusions
來看看下面程式碼:
module M1
end
module M2
include M1
end
module M3
prepend M1
include M2
end
M3.ancestors # => [?, ?, ?]
大家可以想ㄧ想 M3.ancestors的會是什麼呢? 在 module M3裡,先用了 prepend方法引入M1時都沒有問題,但是再用 incldue方法引入M2的時候,這裡的 incldue將會沒有效果,因為 M1已經在 ancestors chain 裡面了。Ruby 會靜靜地忽略第二次的引入方法(不管是 include / prepend),因此在使用引入模組時,最好以ㄧ次為限。
答案是:M3.ancestors # => [M1, M3, M2]
重點回顧
- self 就是當下執行的物件,通常就是方法的『訊息的接收者』
- self 在執行環境頂層、類別(class) 或是 模組(module)時,self 會依所在的位置不同而改變
- ”One step to the right, then up” 物件呼叫方法時,先判斷 Receiver 為何?找到之後先往右一步,再往上找
Reference: The Ruby Object Model from Dave Thomas
Comments