mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Method lookup for refinement super starts with the next active refinement at the super call site, excluding the current refinment, if such a refinement exists. This has been the behavior since Ruby 2.7, and it is now confirmed to be the desired behavior.
303 lines
7.5 KiB
Plaintext
303 lines
7.5 KiB
Plaintext
= Refinements
|
|
|
|
Due to Ruby's open classes you can redefine or add functionality to existing
|
|
classes. This is called a "monkey patch". Unfortunately the scope of such
|
|
changes is global. All users of the monkey-patched class see the same
|
|
changes. This can cause unintended side-effects or breakage of programs.
|
|
|
|
Refinements are designed to reduce the impact of monkey patching on other
|
|
users of the monkey-patched class. Refinements provide a way to extend a
|
|
class locally. Refinements can modify both classes and modules.
|
|
|
|
Here is a basic 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
|
|
|
|
First, a class +C+ is defined. Next a refinement for +C+ is created using
|
|
Module#refine.
|
|
|
|
Module#refine creates an anonymous module that contains the changes or
|
|
refinements to the class (+C+ in the example). +self+ in the refine block is
|
|
this anonymous module similar to Module#module_eval.
|
|
|
|
Activate the refinement with #using:
|
|
|
|
using M
|
|
|
|
c = C.new
|
|
|
|
c.foo # prints "C#foo in M"
|
|
|
|
== Scope
|
|
|
|
You may activate refinements at top-level, and inside classes and modules.
|
|
You may not activate refinements in method scope. Refinements are activated
|
|
until the end of the current class or module definition, or until the end of
|
|
the current file if used at the top-level.
|
|
|
|
You may activate refinements in a string passed to Kernel#eval. Refinements
|
|
are active until the end of the eval string.
|
|
|
|
Refinements are lexical in scope. Refinements are only active within a scope
|
|
after the call to +using+. Any code before the +using+ statement will not have the
|
|
refinement activated.
|
|
|
|
When control is transferred outside the scope, the refinement is deactivated.
|
|
This means that if you require or load a file or call a method that is defined
|
|
outside the current scope the refinement will be deactivated:
|
|
|
|
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
|
|
|
|
If a method is defined in a scope where a refinement is active, the refinement
|
|
will be active when the method is called. This example spans multiple files:
|
|
|
|
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
|
|
|
|
Since the refinement +M+ is active in <code>m_user.rb</code> where
|
|
<code>MUser#call_foo</code> is defined it is also active when
|
|
<code>main.rb</code> calls +call_foo+.
|
|
|
|
Since #using is a method, refinements are only active when it is called. Here
|
|
are examples of where a refinement +M+ is and is not active.
|
|
|
|
In a file:
|
|
|
|
# not activated here
|
|
using M
|
|
# activated here
|
|
class Foo
|
|
# activated here
|
|
def foo
|
|
# activated here
|
|
end
|
|
# activated here
|
|
end
|
|
# activated here
|
|
|
|
In a class:
|
|
|
|
# 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
|
|
|
|
Note that the refinements in +M+ are *not* activated automatically if the class
|
|
+Foo+ is reopened later.
|
|
|
|
In eval:
|
|
|
|
# not activated here
|
|
eval <<EOF
|
|
# not activated here
|
|
using M
|
|
# activated here
|
|
EOF
|
|
# not activated here
|
|
|
|
When not evaluated:
|
|
|
|
# not activated here
|
|
if false
|
|
using M
|
|
end
|
|
# not activated here
|
|
|
|
When defining multiple refinements in the same module inside multiple +refine+ blocks,
|
|
all refinements from the same module are active when a refined method
|
|
(any of the +to_json+ methods from the example below) is called:
|
|
|
|
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 Lookup
|
|
|
|
Method lookup in Ruby is based on the ancestor chain. You can see the
|
|
ancestor chain for any object in Ruby by doing:
|
|
|
|
object.singleton_class.ancestors
|
|
# or, if the object does not support a singleton class:
|
|
object.class.ancestors
|
|
|
|
The ancestor chain is constructed as follows:
|
|
|
|
* Subclasses are before superclasses in the ancestor chain
|
|
* Prepended modules are before the class they prepend in the ancestor
|
|
chain, in reverse order in which they were prepended.
|
|
* Included modules are after the class they are included in in the
|
|
ancestor chain, in reverse order in which they were included.
|
|
|
|
When looking up a method for an object, Ruby goes through each ancestor:
|
|
|
|
* If the class/module has been refined, Ruby will consider the refinements
|
|
activated at the point the method was called, in reverse order of
|
|
activation.
|
|
* Otherwise, Ruby will check the methods of the class/module itself.
|
|
|
|
If no method was found at either point this repeats with the next
|
|
ancestor.
|
|
|
|
Note that methods in a earlier ancestor have priority over refinements in a
|
|
later ancestor. For example, if the method <code>/</code> is defined in a
|
|
refinement for Numeric <code>1 / 2</code> invokes the original Integer#/
|
|
because Integer is a comes before Numeric in the ancestor chain. However,
|
|
if a method +foo+ is defined on Numeric in a refinement, <code>1.foo</code>
|
|
invokes that method since +foo+ does not exist on Integer.
|
|
|
|
== +super+
|
|
|
|
When +super+ is invoked, method lookup starts:
|
|
|
|
* If the method is in a refinement:
|
|
* At the next active refinement in scope at the +super+ call site,
|
|
excluding the current refinement, if such a refinement exists
|
|
* Otherwise, at the refined class or module
|
|
* Otherwise, at the next ancestor
|
|
|
|
Method lookup then proceeds as described in the Method Lookup section
|
|
above.
|
|
|
|
Refinements activated at the call site of a refinement method do not
|
|
affect +super+ inside that method. Only refinements activated at the
|
|
point +super+ was called affect method lookup for that +super+ call.
|
|
You cannot use refinements to insert into the middle of a method
|
|
lookup chain, only to insert at the start of a method lookup chain,
|
|
unless you control the +super+ call sites.
|
|
|
|
Note that if you refine a module, the refinement method can call +super+
|
|
to call the method in the module, but the method in the module cannot
|
|
call +super+ to continue the method lookup process to further ancestors.
|
|
|
|
== Methods Introspection
|
|
|
|
When using introspection methods such as Kernel#method or Kernel#methods refinements are not honored.
|
|
|
|
This behavior may be changed in the future.
|
|
|
|
== Refinement inheritance by Module#include
|
|
|
|
When a module X is included into a module Y, Y inherits refinements from X.
|
|
|
|
For example, C inherits refinements from A and B in the following code:
|
|
|
|
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 in descendants have higher precedence than those of ancestors.
|
|
|
|
== Further Reading
|
|
|
|
See https://github.com/ruby/ruby/wiki/Refinements-Spec for the
|
|
current specification for implementing refinements. The specification also
|
|
contains more details.
|