Chapter 5: Class Definitions - Part I

“Where you learn another way to mix code and bindings at will”― 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 使用外層的變數。

Around Aliases

在 Ruby 的世界裡,你可以使用 Module#alias_method 幫方法取一個新的別名。從 Ruby 的官方文件可以得知 :

alias_method(new_name, old_name)

“Makes new_name a new copy of the method old_name. This can be used to retain access to methods that are overridden.”― Ruby的官方文件 ―

alias_method() 的第一個參數是『新的方法名稱』,第二個參數是『舊的方法名稱』。參數可用符號(symbol)或是字串(string)表示。

class MyClass
 def my_method; 
   "my_method()" 
  end 
  
  alias_method :m, :my_method
end

obj = MyClass.new

obj.my_method        # => "my_method()"
obj.m                # => "my_method()"

建立方法的新別名,可以保留原有方法的功能,同時可依需求來改寫方法並命名更適合的新名稱。

class String
  alias_method :real_length, :length
  def length
    real_length > 5 ? "long" : "short"
  end
end

"How are you".length                # => "long"
"How are you".real_length           # => 11

以上面的範例來說:我們重新定義的 String#length(),但是 real_length()仍然可以參照原始的 length() 方法。其實當重新定義方法時,實際上並不是直接改變方法,而是定義了一個新方法,接著把現有的方法名稱連接上去而已。只要ㄧ直有別名連接該方法,舊版本的方法就可以被呼叫。

Around Alias 也是Ruby 黑魔法之一,可以被解析成以下三個步驟:

  1. 幫方法取別名
  2. 重新定義它
  3. 從新方法內呼叫舊方法

其中一個有關於 Around Alias 的缺點就是當方法被重新定義過後,原本的舊方法有可能其他人更改變動,而造成不必要的誤解。因此你可以幫方法取別名後,再將原本的舊方法改為私有方法,就可以避免掉這個問題。

class String
  alias_method :real_length, :length

  def length
    real_length > 5 ? "long" : "short"
  end

  private :real_length
end

"War and Peace".length # => "long"
"War and Peace".send(:real_length) # => "13"
"War and Peace".real_length # => private method… (NoMethodError)

另ㄧ個比較大的問題是 Around Aliases 其實也是ㄧ種形式上的 Monkey patch,這代表著因為方法被重新定義過了,有可能造成與之前撰寫好的程式碼產生衝突。

明天將介紹 另一個 Ruby 的黑魔法 Refinement,將可以解決 Monkey patch 可能產生的問題。

參考資料