精緻化

由於 Ruby 的開放類別,您可以重新定義或新增功能至現有類別。這稱為「猴子補丁」。不幸的是,此類變更的範圍是全域性的。所有使用猴子補丁類別的使用者都會看到相同的變更。這可能會造成意外的副作用或程式中斷。

精緻化旨在減少猴子補丁對猴子補丁類別的其他使用者的影響。精緻化提供一種在本地擴充類別的方法。精緻化可以修改類別和模組。

以下是基本的精緻化

class C
  def foo
    puts "C#foo"
  end
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

首先,定義類別 C。接著使用 Module#refineC 建立精緻化。

Module#refine 建立一個匿名模組,包含類別的變更或精緻化(範例中的 C)。精緻化區塊中的 self 是這個匿名模組,類似於 Module#module_eval

使用下列方式啟用精緻化

using M

c = C.new

c.foo # prints "C#foo in M"

範圍

您可以在頂層,以及類別和模組中啟用精緻化。您不能在方法範圍中啟用精緻化。精緻化會一直啟用至目前類別或模組定義結束,或是在頂層使用時,至目前檔案結束。

您可以在傳遞給 Kernel#eval 的字串中啟用精緻化。精緻化會一直啟用至 eval 字串結束。

精緻化在範圍中是詞彙的。精緻化只會在呼叫 using 之後的範圍內啟用。using 陳述式之前的任何程式碼都不會啟用精緻化。

當控制權轉移至範圍之外時,精緻化會停用。這表示如果您需要或載入檔案,或呼叫在目前範圍之外定義的方法,精緻化就會停用

class C
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

def call_foo(x)
  x.foo
end

using M

x = C.new
x.foo       # prints "C#foo in M"
call_foo(x) #=> raises NoMethodError

如果在精緻化啟用的範圍中定義方法,當呼叫方法時,精緻化也會啟用。這個範例跨越多個檔案

c.rb

class C
end

m.rb

require "c"

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

m_user.rb

require "m"

using M

class MUser
  def call_foo(x)
    x.foo
  end
end

main.rb

require "m_user"

x = C.new
m_user = MUser.new
m_user.call_foo(x) # prints "C#foo in M"
x.foo              #=> raises NoMethodError

由於精緻化 M 在定義 MUser#call_foom_user.rb 中啟用,因此當 main.rb 呼叫 call_foo 時,也會啟用。

由於 using 是方法,因此精緻化只會在呼叫時啟用。以下是精緻化 M 啟用和未啟用的範例。

在檔案中

# not activated here
using M
# activated here
class Foo
  # activated here
  def foo
    # activated here
  end
  # activated here
end
# activated here

在類別中

# not activated here
class Foo
  # not activated here
  def foo
    # not activated here
  end
  using M
  # activated here
  def bar
    # activated here
  end
  # activated here
end
# not activated here

請注意,如果稍後重新開啟類別 FooM 中的精緻化不會自動啟用。

在 eval 中

# not activated here
eval <<EOF
  # not activated here
  using M
  # activated here
EOF
# not activated here

未評估時

# not activated here
if false
  using M
end
# not activated here

在同一個模組中,於多個 refine 區塊中定義多個精緻化時,當呼叫精緻化方法(以下範例中的任何 to_json 方法)時,來自同一個模組的所有精緻化都會啟用

module ToJSON
  refine Integer do
    def to_json
      to_s
    end
  end

  refine Array do
    def to_json
      "[" + map { |i| i.to_json }.join(",") + "]"
    end
  end

  refine Hash do
    def to_json
      "{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
    end
  end
end

using ToJSON

p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]"

Method 查詢

當查詢類別 C 的執行個體的方法時,Ruby 會檢查

如果在任何一點都找不到方法,則會重複執行 C 的超類別。

請注意,子類別中的方法優先於超類別中的精煉。例如,如果在 Numeric 的精煉中定義了方法 /,則 1 / 2 會呼叫原始的 Integer#/,因為 IntegerNumeric 的子類別,而且會在搜尋超類別 Numeric 的精煉之前進行搜尋。由於方法 / 也存在於子類別 Integer 中,因此方法查詢不會移至超類別。

但是,如果在精煉中於 Numeric 上定義了方法 foo,則 1.foo 會呼叫該方法,因為 foo 不存在於 Integer 中。

super

當呼叫 super 時,方法查詢會檢查

請注意,在精煉方法中呼叫 super 會呼叫精煉類別中的方法,即使在同一個內容中啟用了另一個精煉。這只適用於精煉方法中的 super,不適用於包含在精煉中的模組方法中的 super

方法內省

在使用內省方法(例如 Kernel#method 或 Kernel#methods)時,不會尊重精煉。

此行為可能會在未來變更。

Refinement 透過 Module#include 繼承

當模組 X 包含在模組 Y 中時,Y 會繼承 X 的精煉。

例如,在下列程式碼中,C 繼承 A 和 B 的精緻化

module A
  refine X do ... end
  refine Y do ... end
end
module B
  refine Z do ... end
end
module C
  include A
  include B
end

using C
# Refinements in A and B are activated here.

後代的精緻化優先權高於祖先的精緻化。

進一步閱讀

請參閱 github.com/ruby/ruby/wiki/Refinements-Spec 以取得實作精緻化的最新規格。該規格也包含更多詳細資料。