iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
0
自我挑戰組

Metaprogramming Ruby and Rails系列 第 17

Day 17 -- Instance_eval VS Class_eval

"Where you learn another way to mix code and bindings at will" from Metaprogramming Ruby

Instance_eval VS Class_eval

這兩個方法乍看似乎蠻雷同的,光看方法名稱可猜想也許是差不多的方法,只是ㄧ個是給實例物件用的,另一個給類別用的。但要了解這兩個方法的運作,實際上是比我預想的要複雜許多。

instance_eval( )

BasicObject#instance_eval() 方法會將當下的物件 (self) 轉為接收者(receiver)也就是實例物件(instance object),在實例物件的作用域 (scope) 內,所有的『實例變數』和『私有方法』都可以被存取。

先來看看下面的程式碼:
https://ithelp.ithome.com.tw/upload/images/20201002/20120868TMnEnhLkZt.png

obj.instance_eval() 可以讓你進入到 obj 的作用域內,從第10行可得知當下的物件(self)是 MyClass的實例物件,因此在第11行可以讀取到實體 @v 的值。
注意到行數5以後都沒有 Scope Gates 的關鍵字 (Class/Module/Def),因此你可以帶著 v=2 進入第17行的 block,再傳給 instance_eval()方法。

Define Methods inside instance_eval( )

接著定義 my_method()方法在 block 裡:
https://ithelp.ithome.com.tw/upload/images/20201002/20120868UUGVVgMvyy.png
當定義 my_method() 在 (do…end) block 裡並且傳進 instance_eval(),這個 my_method() 就會變成 obj 物件專屬的方法,也就是之前介紹過的 singleton method。

instance_exec( )

相較於 instance_eval() 方法, instance_exec() 方法更加靈活多了因為多了一個可以傳入引數給 block 的可能性。
https://ithelp.ithome.com.tw/upload/images/20201002/20120868C7UAIX6W1V.png
也許你會疑惑在14行為什麼 @y 沒有顯示出來呢?原因是 instance_eval() 會將 self 設為 C 類別的實例物件 (instance object of C),如果你檢視物件的作用域,很明顯只有 @x 這個實例變數。因此 @y 會是 nil,才會顯示空白。
你可以選擇將原本的實例變數 @y 改設為區域變數 y,這樣的話 block 就看得到 y,@y 的輸出值就會是 2。

y = 2
C.new.instance_eval { "@x: #{@x}, @y: #{y}" }
D.new.twisted_method # => "@x: 1, @y: 2"

比較好的做法是呼叫 instance_eval() 方法,讓 @x 和 @y 合併在相同的作用域下,就可以傳 @y 進入 block 了。

@y = 2
C.new.instance_exec(@y) { |y| "@x: #{@x}, @y: #{y}" }
D.new.twisted_method # => "@x: 1, @y: 2"

class_eval( )

Module#class_eval() 方法會將當下的物件 (self) 轉設定為類別 (class),當下的類別 (current class) 就會被打開,就與先前提到的 Open Class 相似,你可以在類別內新增或是複寫方法。
換句話說, instance_eval() 方法是只能對單一物件做改變,如果你要對每一個實例物件做綁定就可以使用 Module#class_eval() 方法。

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new
obj2 = MyClass.new

obj.instance_eval do
  # Getter
  def v
    @v
  end
end

p obj.v         # 1
p obj2.v        # undefined method `v' for...(NoMethodError)

instance_eval() 只能綁定單一物件,但如果要每一個物件都做 getter 方法就太麻煩了!
試試看 class_eval() 方法

class MyClass
  def initialize
    @v = 1
  end
end
obj = MyClass.new
obj2 = MyClass.new
MyClass.class_eval do
  # Getter
  def v
    @v
  end
end

p obj.v         # 1
p obj2.v        # 1

這樣就其實同等於在 MyClass 類別裡新增一個實例方法

class MyClass
  def initialize
    @v = 1
  end
  # Getter
  def v
    @v
  end
end

另外要注意的是以往使用 Open Class 技巧去存取類別(class),我們是需要用關鍵字 class + 常數名稱去重新打開類別,這代表同時也開啟的新的作用域(scope)跟失去了舊的 bindings。但是如果用 class_eval()方法來打開類別,因為有 Flat Scope 的特性,則是可以在 class_eval 的 block 使用外層的變數。


上一篇
Day 16 -- Callable Object: Proc 、Lambda 、Method
下一篇
Day 18 -- Around Aliases
系列文
Metaprogramming Ruby and Rails33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言