Refinements (精炼)
由于 Ruby 的开放类,您可以重定义或向现有类添加功能。这被称为“Monkey Patch”(猴子补丁)。不幸的是,这类更改的作用域是全局的。所有使用猴子补丁类的用户都会看到相同的更改。这可能导致意外的副作用或程序损坏。
Refinements 的设计目的是减少猴子补丁对被猴子补丁类的其他用户的影响。Refinements 提供了一种局部扩展类的方法。Refinements 可以同时修改类和模块。
这是一个基本的 Refinement
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#refine 创建一个 C 的 Refinement。
Module#refine 创建一个匿名模块,其中包含对类(示例中的 C)的更改或 Refinements。在 refine 块中,self 是这个匿名模块,类似于 Module#module_eval。
使用 using 激活 Refinement
using M c = C.new c.foo # prints "C#foo in M"
Scope
您可以在顶层、类内部和模块内部激活 Refinements。您不能在方法作用域内激活 Refinements。Refinements 会一直激活到当前类或模块定义的结束,或者在顶层使用时,直到当前文件的结束。
您可以在传递给 Kernel#eval 的字符串中激活 Refinements。Refinements 在 eval 字符串的结束之前都是有效的。
Refinements 是词法作用域的。Refinements 在调用 using 之后的作用域内才有效。using 语句之前的任何代码都不会激活 Refinement。
当控制转移到作用域之外时,Refinement 会被解除激活。这意味着,如果您 require 或 load 一个文件,或者调用一个在当前作用域之外定义的方法,Refinement 将被解除激活。
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
如果在 Refinement 激活的作用域中定义了一个方法,那么当调用该方法时,Refinement 也会被激活。此示例跨越多个文件
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
由于 Refinement M 在定义 MUser#call_foo 的 m_user.rb 中是激活的,因此当 main.rb 调用 call_foo 时,它也是激活的。
由于 using 是一个方法,Refinements 仅在其被调用时才有效。以下是 Refinement 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 中的 Refinements **不会** 自动激活。
在 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 块中定义多个 Refinements 时,在调用被 Refined 的方法(例如下面的 to_json 方法)时,来自同一个模块的所有 Refinements 都是激活的。
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}]"
方法查找
在查找类 C 实例的方法时,Ruby 会检查:
-
C的 Refinements,按激活的倒序排列 -
C的 prepend 模块 -
C -
C的 include 模块
如果在任何点都没有找到方法,则重复执行此过程,直到找到 C 的超类。
请注意,子类中的方法具有比超类中的 Refinements 更高的优先级。例如,如果方法 / 在 Numeric 的 Refinement 中定义,1 / 2 会调用原始的 Integer#/,因为 Integer 是 Numeric 的子类,并且在搜索超类 Numeric 的 Refinements 之前被搜索。由于方法 / 也存在于子类 Integer 中,所以方法查找不会向上移动到超类。
但是,如果一个方法 foo 在 Numeric 的 Refinement 中定义,1.foo 会调用该方法,因为 foo 在 Integer 中不存在。
super
当调用 super 时,方法查找会检查:
-
当前类的 include 模块。请注意,当前类可能是一个 Refinement。
-
如果当前类是一个 Refinement,方法查找将按照上面的“方法查找”部分的说明进行。
-
如果当前类有一个直接的超类,方法查找将按照上面的“方法查找”部分的说明,使用超类进行。
请注意,Refinement 中的方法里的 super 会调用被 Refined 类中的方法,即使在同一个上下文中激活了另一个 Refinement。这仅适用于 Refinement 中的方法里的 super,不适用于包含在 Refinement 中的模块里的方法里的 super。
方法内省
在使用内省方法(如 Kernel#method 或 Kernel#methods)时,不考虑 Refinements。
此行为将来可能会更改。
通过 Module#include 继承的 Refinement
当模块 X 被包含到模块 Y 中时,Y 会继承 X 的 Refinements。
例如,在以下代码中,C 继承了 A 和 B 的 Refinements:
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.
后代中的 Refinements 比祖先中的 Refinements 具有更高的优先级。
进一步阅读
有关实现 Refinements 的当前规范,请参阅 github.com/ruby/ruby/wiki/Refinements-Spec。该规范还包含更多详细信息。