iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 8
0
自我挑戰組

Metaprogramming Ruby and Rails系列 第 8

Day 8 -- What Happens When You Call a Methods? Part II

昨天談到了 Ancestor chain ,文章最後留下了:

Cat.ancestors # => [Cat, Animal, Object, Kernel, BasicObject]

在詢問 Cat 類別的繼承關係時,可以看出回傳陣列裡明明就有 Kernel,為什麼在 Method Lookup Diagram 中卻沒有畫出來呢?今天的主題就要介紹 Kernel 和 Method Lookup with modules。

What is Kernel?

Kernel 在 Ruby 程式語言裡,是模組(Module)且被 included 在 Object 類別內,因此每一個 Ruby object 都有 Kernel 模組的實例方法。例如:我們常見的 puts 方法就是來自 Kernel 模組的私有實例方法。

Kernel.private_instance_methods.grep(/^puts/)  # => [:puts]

同樣地,我們可以利用開放類別的技巧覆寫或是新增方法,給所有的 Ruby 物件。

module Kernel
  def puts(*args)
    "Wrong puts, sorry"
  end
end
puts "abc"  # => "Wrong puts, sorry"

Method Lookup with modules

在能進一步討論 Method Lookup 之前,首先要認識使用 extend、include and prepend 方法引入類別/模組有何不同:

include

module Kung_Fu
  def kick
    "Knockout Kick"
  end
end
class Master
  include Kung_Fu
end

class Disciple < Master
end

以繼承關係圖來看,當師傅(Master)類別 include 功夫(Kung_Fu)模組進來時,Ruby 會將插入Kung_Fu 模組在 Master 類別之上,並且Kung_Fu 模組內的方法都會變成是 Master class 的 instance method

Disciple.ancestors  # => [Disciple, Master, Kung_Fu, Object...]

prepend

從 Ruby 2.0 開始,除了 include 方法之外,也可用 prepend 方法引入 module,並會放在下方。

module Boxing
  def punch
    "Knockout Punch"
  end
end
class Coach
  prepend Boxing
end
class Trainee < Coach
end

Trainee.ancestors  # => [Trainee, Boxing, Coach, Object...]

https://ithelp.ithome.com.tw/upload/images/20200923/20120868u265NdSGmS.png

extend

目前為止不管是 include 還是 prepend 都是從模組裡引入『實例方法』,但是如果要引入『類別方法』就需要用 Object#extend 方法來導入:

module Boxing
  def punch
    "Knockout Punch"
  end
end

class Coach
  extend Boxing
end

class Trainee < Coach
end

Coach.punch                   # => "Knockout Punch"
Coach.singleton_methods       # => [:punch]
Trainee.ancestors             # => [Trainee, Coach, Object, Kernel, BasicObject]

進階補充

補充的內容會需要先了解 Chapter 3 裡的 Singleton Method,所以晚點再回來看進階的部分會較適合。

在上面的程式碼最後一行所回傳的值似乎有點奇怪,竟然沒有 Boxing 的蹤影。

Trainee.ancestors  # => [Trainee, Coach, Object, Kernel, BasicObject]

如果你已經看完了全部 Chapter 3,你或許就會猜到其實是 Ruby 把 Boxing 藏在 singleton class 裡了,當我們用 extend 將 Boxing 模組引入到 Coach 類別裡,實際上是打開 Coach 的 singleton class 並且 include 引入了 Boxing 模組。

我們可以從以下程式碼來驗證:

module Boxing
  def punch
    "Knockout Punch"
  end
end

class Coach
  class << self
    include Boxing
  end
end

Coach.singleton_methods       # => [:punch]
Trainee.singleton_methods     # => [:punch]

Coach.punch       # => "Knockout Punch"
Trainee.ancestors  # => [Trainee, Coach, Object, Kernel, BasicObject]

Multiple Inclusions

來看看下面程式碼:

module M1
end
module M2
  include M1
end
module M3
  prepend M1
  include M2
end
M3.ancestors   # => [?, ?, ?]

大家可以想ㄧ想 M3.ancestors的會是什麼呢?
在 module M3裡,先用了 prepend方法引入M1時都沒有問題,但是再用 incldue方法引入M2的時候,這裡的 incldue將會沒有效果,因為 M1已經在 ancestors chain 裡面了。Ruby 會靜靜地忽略第二次的引入方法(不管是 include / prepend),因此在使用引入模組時,最好以ㄧ次為限。
答案是:M3.ancestors # => [M1, M3, M2]


上一篇
Day 7 -- What Happens When You Call a Methods? Part I
下一篇
Day 9 -- Singleton class In Ruby 神秘的匿名者 PART I
系列文
Metaprogramming Ruby and Rails33

尚未有邦友留言

立即登入留言