Quiz: Missing Lines

題目說明:

下面程式碼以圖表方式呈現,可看出物件與類別之間的關聯:

class MyClass
end

obj1 = MyClass.new
obj2 = MyClass.new
From Metaprogramming Ruby 2nd
請回答以下問題:
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 的圖就會變成這樣:

From Metaprogramming Ruby 2nd

還記得 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. 重新命名 Printable 模組裡的 print() 方法: 如此一來 method lookup 就只會在 Document 找到 print()。
  2. 把 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(),才造成了無窮迴圈的問題。

解決方案

  1. 快速但認為不是很好的解法,設定 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
    
  2. 書中提供的方法
    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

補充說明

  1. The class of Object is Class 

  2. The superclass of Module is Object 

  3. The class of Class is Class 

  4. From Metaprogramming Ruby 2nd