Ruby Object Model

Ruby 中的物件模型完整介紹與筆記

前言:Ruby 中的物件導向

Ruby Object Model 是我覺得 Ruby 算是十分特別也是十分重要的概念。

在一般的物件導向程式語言中,物件是類別的實體,而資料型態是資料型態。一個變數可以儲存一個資料型態,一個變數也可以指向一個物件,但資料型態和物件是兩回事。

但在 Ruby 中,所有的東西都是物件

所有東西都是物件

對,在 Ruby 中,主程序(Main)本身是一個物件,被定義好的 function 是物件,你所知道的資料型態如:整數、浮點、Boolean、字串(Ruby 中沒有字串與字元的分別)等是物件,就連平常代表虛無的 nil 也是一個物件!

打開 irb 試試看:

3.0.1 :001 > a = nil
 => nil 
3.0.1 :002 > a.nil?
 => true 
3.0.1 :003 > nil.class
 => NilClass 

因為 Ruby 中 nil 是一個 NilClass 的 instance(也是唯一的 instance,不能 new 一個出來),所以有 nil? 這種方法,可以問一個變數是不是 nil,夠神奇吧?
這個語法糖相當於其他語言的 a == nil 或是 a is None,但變成 method 的形式讀起來更直觀。

所有 Functions 都是 Methods

因為所有東西都是物件,在主程序以 def 定義的 function,其實也只是掛在 Main object 下的 private method:

3.0.1 :001 > def my_custom_method
3.0.1 :002 >   puts 'hello'
3.0.1 :003 > end
 => :my_custom_method

# 注意如果用 methods 只會拿到 public 和 protected methods
# 這邊的 self 其實是可以省略的,但為了強調我們是在存取 `main` 這個 object,先保留了:
3.0.1 :004 > self.private_methods.include? :my_custom_method
 => true

而 Main object 已經在使用了,還可以後續再寫入方法,這也是 ruby 的特色之一。

Ruby Object Model

Ruby 的物件模型,也就是 Ruby 中物件導向的運作機制。

Instance

在 Ruby 中,可以利用 Class#new 幫一個類別建立新的 instance,這時 instance 內儲存的資訊有:

  • instance_variable
  • class
  • object_id

Instance methods 不需要存在每個物件裡,只需要存在 class 並查找 instance methods 就好。

Self

在 ruby 中,永遠都會有一個代表「現在的物件」,可以透過 self 取得:

puts self
# => main,在最外層,self 代表 main object

class Car
  puts self
  # => Car,在 class 內,self 是當下的 class object
  def run
    puts "#{self} run! pupu!"
    # 在 method 內,self 是物件本身!
  end
end

c = Car.new
c.run # => #<Car:0x00007fe17f8eeac8> run! pupu!

而這邊還有個有趣的觀察:
在 ruby 中,定義 class 是當下就會執行,而 def 定義 method 則是使用時動態執行。

Singleton Class

Singleton Class 是跟著每個 ruby 物件自動產生,對於每個物件是獨一無二的 Class。
會有這樣的設計,要回到剛剛說的 ruby 的物件不會儲存 instance method 的特性。

因為物件不存 method,但 ruby 又有個特色,可以為現有的物件定義新方法:

class << c
  def run_twice
    run
    run
  end
end
c.run_twice 
# #<Car:0x00007fe17f8eeac8> run! pupu!
# #<Car:0x00007fe17f8eeac8> run! pupu!

這時候這個 run_twice 方法是只有 c 這個物件擁有,其他的 Car 的 instance 是不會擁有的,因此 run_twice 不能被放在 Car 這個 class 裡。

在 ruby 中,這種單一物件特有的 method,就叫做 singleton method:

c.singleton_methods
# => [:run_twice]

而 singleton class 就是拿來儲存 singleton method 用的。

因為 singleton class 是獨一無二的,所以有幾個限制:

  • 不能建立新的 instance(畢竟要是獨一無二,才叫 singleton)
  • 不能被繼承(但這個有例外,就是 class 的 singleton class,會自動跟著 class 自己產生繼承鏈(見下方),只是仍然不能手動去繼承他,不然會扔出 TypeError

對於一般 object (並不是 Class 的 instance)來說,每個物件都有 singleton class,因此會在後面加上物件本身的識別,並繼承自他的 class:

c.singleton_class
# => #<Class:#<Car:0x00007fe17f8eeac8>>
c.singleton_class.superclass == c.class
# => true

對於一般 class(Class 的 instance),因為 class 本身為為一,所以 singleton class 也是唯一的,並繼承自其 superclass 的 singleton class。
也就是 class 的 singleton class 們是形成一條繼承鏈,直到最後繼承自 Class。

Car.singleton_class.superclass == Car.class
# => false

Car.superclass.singleton_class == Car.singleton_class.superclass
# => true

Car.singleton_class.ancestors
# => [#<Class:Car>, #<Class:Object>, #<Class:BasicObject>,
#     Class, Module, Object, Kernel, BasicObject]

Receiver

當我們呼叫一個 method,會需要有一個 receiver,使用 receiver.method_name 的形式調用 method。
以上面 c.run 為例,這時的 receiver 就是 c
如果沒有指定 receiver,預設會使用 self,也就是當下的物件。

而 Ruby 的 private method 的定義,即是不能明確指定 receiver,換句話說,就是只能透過 self 來呼叫(當然如果你指定成 self 還是會噴 error 的喔 XD)。
不過 Ruby 這麼自由的語言,當然可以繞過去,透過 receiver.send(:method_name) 的方式即可呼叫 private method。

Ruby 的哲學就是把給開發者最大的自由,當然也需要開發者謹慎的使用。

Method Lookup

當我們呼叫一個物件的 method,ruby 會去看該物件的 class 有沒有該方法定義。
而 class 的查找順序為:

  1. Singleton class
  2. 繼承鏈中的 class 一路上找 (Class#ancestors 可以拿出繼承鏈,不過會把 Module 也包含在內)

而對於每個 class C而言,再以以下順序查找:

  1. Refinements,LIFO(後啟用,優先查找)
  2. prepended module
  3. 自身定義的 methods
  4. included module

而每個 Refinements 內部也會依照 prepend -> 直接定義 -> include 的順序來查找。

如果到了繼承鏈底部,都沒有找到該方法,會回到 singleton class 並重新查找 method_missing(這也是透過 method_missing 定義的 ghost method 會比較慢的原因:要爬兩次繼承鏈)。

上面就是目前 Ruby 的整個 method 查找過程。

整理一下,可以分解成三層:

  1. class 層:根據 singleton class -> superclass -> superclass 的順序查找。
  2. Refinement 層:若有 Refinements,則依照 LIFO 順序查找,最後才是該 class 自己。
  3. 最內層:prepend -> def -> include。

簡化版本:

一般看到的是沒有 refinements 的版本,而且 refinements 我自己也不常用 XD,拿掉也比較簡單:

  1. singleton class -> superclass -> superclass …
  2. prepend -> def -> include

所以一個 instance 的 method 組成,其實就是 singleton methods + 繼承鏈上的所有 instance methods + refinements 時定義的 methods(當然,不包含用 method_missing 動態處理的 ghost methods)。

# 因為 refinement 的 method 無法透過 Kernel#methods 取得,因此先忽略
# 這裡示範了 singleton methods + 繼承鏈上的所有 instance methods = instance 的 methods
c.methods == [c.singleton_class, *Car.ancestors].map { |a| a.instance_methods(false) }.reduce(:+)
# => true

附帶一提,當呼叫 class method 時,其實也是一樣的,因為在 ruby 中 class 也是個物件,所以也是先找 singleton class,然後隨著繼承鏈上找。

例如:

以下面這個 MyClass 為例:

class MyClass < ParentClass
end

instance = MyClass.new

class 與 superclass 分別為:

Name Class Superclass
instance MyClass
instance 的 singleton class Class MyClass
MyClass Class ParentClass
ParentClass Class Object
Object Class BasicObject
BasicObject Class nil

畫成圖:

                BasicObject
                    ^
                    |
                Object
                    ^
                    |
                ParentClass
                    ^
                    |
                MyClass
                    ^
                    |
instance  ->  singelton_class

(參考自 https://gist.github.com/damien-roche/351bf4e7991449714533#one-more-step 的圖並小修改)

所以有個 method lookup 的口訣是「往右,然後一直往上」,其實就是:

  • 往右:instance 的 singleton_class
  • 往上:superclass -> superclass …

參考資料