下面程式碼以圖表方式呈現,可看出物件與類別之間的關聯:
class MyClass
end
obj1 = MyClass.new
obj2 = MyClass.new
obj3 = MyClass.new obj3.instance_variable_set('@x', 10)
請先看以下程式碼:
module Printable
def print
"Printable#print"
end
def prepare_cover
# ...
end
end
module Document
def print_to_screen
prepare_cover
format_for_screen
print
end
def format_for_screen
# ...
end
def print
"Document#print"
end
end
class Book
include Document
include Printable
# ...
end
當我們建立了 Book 的實例物件 b,並呼叫 print_to_screen()方法。
b = Book.new
b.print_to_screen
假設這個時候問題出現了,因為輸出的字串是錯誤的,這代表呼叫了不對的 print()方法
首先我們可以請 Ruby 給點提示:
Book.ancestors # => [Book, Printable, Document, Object, Kernel, BasicObject]
從提示裡,我們得知 ancestor's chain 正確的面貌。在問題中並沒有特別說 Book 是繼承自哪個類別,因此如果沒有引入任何模組的話,在預設情況下,Book 是繼承自 Object。
但是當 Book 用 include 引入了 Document 模組,Ruby 會把 Document 模組加在 Book 的上面,也就成為 Book 的父層。接著又再 include 了 Printable 模組,Ruby 會再把 Printable 模組插入在 Book 的上面。此時的 ancestor's chain 的圖就會變成這樣:
還記得 Chapter 2 介紹的 Method Lookup 嗎?
當呼叫 b.print_to_screen 時,實例物件 b 會變成是當下執行的物件,也就是 self。接著依 『 黃金法則:往右ㄧ步,再往上 』 "One step to the right, then up",往右會先找到 Book 類別,再往上ㄧ直找到 Document#print_to_screen()。
進入 print_to_screen() 內,就準備開始執行裡面的方法。不過 print_to_screen()裡面的方法(包括 print)都沒有明確的接收者(receiver),因此 method lookup 只能再次從 Book 往上找。以 print() 方法來說,最接近 Book 的 print() 方法會在 Printable 模組找到。
要解決掉 Bugs 可以有兩種做法:
請找出以下程式碼的錯誤並加以修正:
1 class Roulette
2 def method_missing(name, *args)
3 person = name.to_s.capitalize
4 3.times do
5 number = rand(10) + 1
6 puts "#{number}..."
7 end
8 "#{person} got a #{number}"
9 end
10 end
number_of = Roulette.new
puts number_of.bob
puts number_of.frank
正確的輸出應該是這樣:
# 5...
# 6...
# 10...
# Bob got a 10
# 7...
# 4...
# 3...
# Frank got a 3
如果我們直接拿以上錯誤程式碼去跑跑看,會得到類似下面無限迴圈的錯誤訊息 (SystemStackError)。
找出這個錯誤訊息應該不算是太難,但是要了解錯誤發生的原因就不是那麼淺顯易見了。首先可以看出 number 變數是在 do...end 的 block 中定義的,然後會再傳給 times() 方法。
還記得在 Chapter 4 中提到 Scope 的概念,在block內定義的變數,其作用域只限於 block 裡。因此當程式跑到第8行的 number 時,Ruby 其實會認為 number 是方法,而不是在第 5 行所定義的變數。
假設我們在 Roulette 類別內定義的方法不是命名為 method_missing,其實錯誤訊息就是常見的名稱錯誤(NameError),但題目的陷阱或是有趣的地方就在於方法名稱是 method_missing。
我們知道當 Ruby 找不到方法或是無法回應當物件時,就會呼叫 BasicObject 所內建 method_missing() 方法,因此程式跑到第 8 行的 number 時,會錯認為是方法,又在 self (Roulette的實例物件) 裡找不到 number 方法,所以呼叫 method_missing(),才造成了無窮迴圈的問題。
分享自己快速但覺得不是很好的解法:設定 number 為全域變數,程式就不會錯認為第 8 行的 number 是方法了。
1 class Roulette
2 def method_missing(name, *args)
3 person = name.to_s.capitalize
4 3.times do
5 $number = rand(10) + 1
6 puts "#{$number}..."
7 end
8 "#{person} got a #{$number}"
9 end
10 end
書中提供的方法
1 class Roulette
2 def method_missing(name, *args)
3 person = name.to_s.capitalize
4 super unless %w[Bob Frank Bill].include? person
5
6 number = 0
7 3.times do
8 number = rand(10) + 1
9 puts "#{number}..."
10 end
11 "#{person} got a #{number}"
12 end
13 end
本篇所有題目都是來自於 Metaprogramming Ruby 2nd