Quiz: Missing Lines
題目說明:
下面程式碼以圖表方式呈現,可看出物件與類別之間的關聯:
class MyClass
end
obj1 = MyClass.new
obj2 = MyClass.new
請回答以下問題:
1. What’s the class of Object? 答案1
2. What’s the superclass of Module? 答案2
3. What’s the class of Class? 答案3
4. 執行以下程式碼並畫出以上答案的關聯? 答案4
</figure>
obj3 = MyClass.new obj3.instance_variable_set('@x', 10)
Quiz: Tangle of Modules
題目說明:
請先看以下程式碼:
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()方法,這個時候問題出現了,因為輸出的字串是錯誤的,這代表呼叫了不對的 print()方法。
b = Book.new
b.print_to_screen
請回答以下問題:
1. 請問我們到底呼叫了哪ㄧ個版本的 print()? Printable or Document?
2. 請在紙上畫出 ancestors chain
3. 請幫忙解 Bug,讓 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 可以有兩種做法:
- 重新命名 Printable 模組裡的 print() 方法: 如此一來 method lookup 就只會在 Document 找到 print()。
- 把 Book 類別內引入模組的順序互換: 讓 Document 模組變成最接近 Book 就會先在 Document 找到 print()。
Quiz: Bug Hunt
題目說明:
請找出以下程式碼的錯誤並加以修正:
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
Comments