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。

https://ithelp.ithome.com.tw/upload/images/20200920/20120868QZtlpXI8Zf.png

以上面程式碼為例: 當我們呼叫 speak() 方法的時候,『訊息的接收者』為 cat 這個實體物件,也就是當下執行的物件 self。

@age 是 cat 的實體變數,因此在行數 4 可以看到 @age 會顯示數字 5。 接者執行 count() 此時,self 維持不變,也就是說 @age ㄧ樣是 cat 的實體變數。最後找到 count() 執行第10行後,回傳數字 6 在第 5 行。

Self 在以下兩種例外情況則需要特別地注意:

1. The Top Level

此物件當下是在 ruby 執行環境下的最頂層,有可能是還沒有開始呼叫 method 或者是所有的 method 都已經執行完成。 https://ithelp.ithome.com.tw/upload/images/20200920/20120868tddt86kVbV.png 參考自 Metaprogramming Ruby

2. Class / Module Definition and self

當 self 在類別(class)或模組(module)時,self 會依所在的位置不同而改變。 https://ithelp.ithome.com.tw/upload/images/20200920/201208681jWb7fPUdd.png 以上面的程式碼來說,雖然第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 件事情:

  1. Ruby 會依 “ 查詢方法流程 (method lookup)” 尋找被指定的方法
  2. 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() 方法被找到為止。

https://ithelp.ithome.com.tw/upload/images/20200922/20120868AsaLPghfNU.png

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]

https://ithelp.ithome.com.tw/upload/images/20200923/20120868u265NdSGmS.png

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