前言: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 的查找順序為:
- Singleton class
- 繼承鏈中的 class 一路上找 (
Class#ancestors
可以拿出繼承鏈,不過會把 Module 也包含在內)
而對於每個 class C
而言,再以以下順序查找:
- Refinements,LIFO(後啟用,優先查找)
- prepended module
- 自身定義的 methods
- included module
而每個 Refinements 內部也會依照 prepend -> 直接定義 -> include 的順序來查找。
如果到了繼承鏈底部,都沒有找到該方法,會回到 singleton class 並重新查找 method_missing
(這也是透過 method_missing
定義的 ghost method 會比較慢的原因:要爬兩次繼承鏈)。
上面就是目前 Ruby 的整個 method 查找過程。
整理一下,可以分解成三層:
- class 層:根據 singleton class -> superclass -> superclass 的順序查找。
- Refinement 層:若有 Refinements,則依照 LIFO 順序查找,最後才是該 class 自己。
- 最內層:prepend -> def -> include。
簡化版本:
一般看到的是沒有 refinements 的版本,而且 refinements 我自己也不常用 XD,拿掉也比較簡單:
- singleton class -> superclass -> superclass …
- 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 …
參考資料
- Metaprogramming Ruby 2, Paolo Perrotta
- 我同事 Anthony 大神的 Metaprogramming Ruby 筆記,感謝他 XD
- Ruby method lookup path for an object
- A Primer on Ruby Method Lookup