精化

由于 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 激活精化

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

请注意,如果稍后重新打开类 Foo,则 M 中的细化不会自动激活。

在 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 以了解实现细化的当前规范。该规范还包含更多详细信息。