假设我正在类中进行Monkey Patching,我如何从覆盖的方法中调用被覆盖的方法?即类似于super
的东西。
例如:
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
假设我正在类中进行Monkey Patching,我如何从覆盖的方法中调用被覆盖的方法?即类似于super
的东西。
例如:
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
编辑:自我写下这个答案已经9年了,现在需要一些整容来保持时效性。
你可以在这里查看编辑前的版本。
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
如果您控制Foo
对象的创建,这将起作用。只需更改每个创建Foo
的地方,以创建一个ExtendedFoo
。如果您使用依赖注入设计模式、工厂方法设计模式、抽象工厂设计模式或类似的东西,那么这将更加有效,因为在这种情况下,您只需要更改一个地方。
如果您不能控制Foo
对象的创建,例如因为它们是由框架创建的,而该框架位于您无法控制的范围之外(例如ruby-on-rails),那么您可以使用包装器设计模式:
require 'delegate'
class Foo
def bar
'Hello'
end
end
class WrappedFoo < DelegateClass(Foo)
def initialize(wrapped_foo)
super
end
def bar
super + ' World'
end
end
foo = Foo.new # this is not actually in your code, it comes from somewhere else
wrapped_foo = WrappedFoo.new(foo) # this is under your control
wrapped_foo.bar # => 'Hello World'
Foo
对象进入您的代码时,您将其包装到另一个对象中,然后在代码中其他所有地方使用 该 对象而不是原始对象。delegate
库中的 Object#DelegateClass
帮助方法。Module#prepend
:Mixin PrependingModule#prepend
被添加以支持更多或更少的这种用例。 Module#prepend
与Module#include
执行相同的操作,除了它直接将mixin混合到类下面:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
prepend FooExtensions
end
Foo.new.bar # => 'Hello World'
Module#prepend
的内容:Ruby模块prepend与派生
我看到有些人尝试(并在StackOverflow上问为什么它不起作用),例如像这样include
一个混入,而不是prepend
它:
class Foo
def bar
'Hello'
end
end
module FooExtensions
def bar
super + ' World'
end
end
class Foo
include FooExtensions
end
super
。然而,Module#include
将mixin插入到继承层次结构中类的上方,这意味着FooExtensions#bar
永远不会被调用(如果它被调用,super
实际上不会指向Foo#bar
,而是指向不存在的Object#bar
),因为Foo#bar
总是被优先找到。
最重要的问题是:我们如何保留bar
方法,而不必实际保留一个实际的方法?答案常常在函数式编程中。我们将该方法作为一个实际的对象保存,并使用闭包(即块)来确保只有我们自己持有该对象:
class Foo
def bar
'Hello'
end
end
class Foo
old_bar = instance_method(:bar)
define_method(:bar) do
old_bar.bind(self).() + ' World'
end
end
Foo.new.bar # => 'Hello World'
old_bar
只是一个局部变量,它将在类体的末尾超出范围,并且无法从任何地方访问,即使使用反射!而且由于Module#define_method
接受一个块,并且块关闭其周围的词法环境(这就是为什么我们在这里使用define_method
而不是def
),它(以及仅它)仍然可以访问old_bar
,即使它已经超出了范围。old_bar = instance_method(:bar)
bar
方法包装成一个UnboundMethod
方法对象,并将其分配给本地变量old_bar
。这意味着,即使bar
被覆盖,我们现在也有一种方法来保存它。old_bar.bind(self)
self
。换句话说:方法总是知道它被调用的对象,它知道它的 self
是什么。但是,我们直接从类中获取了方法,它怎么知道它的 self
是什么呢?UnboundMethod
bind
到一个对象上,这将返回一个 Method
对象,然后我们可以调用它。(UnboundMethod
无法被调用,因为它们不知道如何在不知道自己的 self
的情况下执行。)bar
一样表现!bind
返回的Method
。在Ruby 1.9中,有一些新的巧妙语法可以做到这一点(.()
),但如果您使用的是1.8版本,则可以简单地使用call
方法;无论如何,.()
都会被翻译成它。
以下是其他一些问题,其中解释了其中一些概念:
alias_method
链class Foo
def bar
'Hello'
end
end
class Foo
alias_method :old_bar, :bar
def bar
old_bar + ' World'
end
end
Foo.new.bar # => 'Hello World'
Foo.new.old_bar # => 'Hello'
old_bar
方法污染了命名空间。这个方法将出现在我们的文档中,在IDE的代码完成中显示,在反射期间也会出现。此外,它仍然可以被调用,但我们可能已经使用猴子补丁对其进行了修改,因为我们一开始就不喜欢它的行为,所以我们可能不想让其他人调用它。Module#alias_method_chain
得到了普及。Module#prepend
示例。class Foo
def bar
'Hello'
end
end
module ExtendedFoo
module FooExtensions
def bar
super + ' World'
end
end
refine Foo do
prepend FooExtensions
end
end
Foo.new.bar # => 'Hello'
# We haven’t activated our Refinement yet!
using ExtendedFoo
# Activate our Refinement
Foo.new.bar # => 'Hello World'
# There it is!
您可以在这个问题中看到一个更复杂的使用细化的例子:如何为特定方法启用猴子补丁?
在 Ruby 社区确定使用 Module#prepend
之前,有多种不同的想法浮现出来,你可能会在早期讨论中偶尔看到这些想法的参考。所有这些想法都被 Module#prepend
所包含。
其中一个想法是来自 CLOS 的方法组合器。这基本上是一种子集面向方面编程的非常轻量级版本。
使用类似于
class Foo
def bar:before
# will always run before bar, when bar is called
end
def bar:after
# will always run after bar, when bar is called
# may or may not be able to access and/or change bar’s return value
end
end
bar
方法的执行。bar:after
中访问bar
的返回值。也许我们可以(滥用)super
关键字?class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
before组合器相当于在一个覆盖方法的mixin中使用prepend
,并在方法的最后调用super
。同样地,after组合器相当于在一个覆盖方法的mixin中使用prepend
,并在方法的开头调用super
。
您还可以在调用super
之前和之后做一些事情,可以多次调用super
,并且检索和操作super
的返回值,使prepend
比方法组合器更强大。
class Foo
def bar:before
# will always run before bar, when bar is called
end
end
# is the same as
module BarBefore
def bar
# will always run before bar, when bar is called
super
end
end
class Foo
prepend BarBefore
end
和
class Foo
def bar:after
# will always run after bar, when bar is called
# may or may not be able to access and/or change bar’s return value
end
end
# is the same as
class BarAfter
def bar
original_return_value = super
# will always run after bar, when bar is called
# has access to and can change bar’s return value
end
end
class Foo
prepend BarAfter
end
old
关键字该想法添加了一个类似于 super
的新关键字,它允许您以与 super
相同的方式调用 被覆盖 方法,就像 super
允许您调用 被覆盖 方法一样:
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
old
的方法,你将无法再调用它!prepend
混入中重写方法中的super
本质上与这个提议中的old
相同。
redef
关键字def
,而是添加一个新的关键字来重新定义方法。这是向后兼容的,因为当前的语法已经是非法的:class Foo
def bar
'Hello'
end
end
class Foo
redef bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
我们可以重新定义redef
内部super
的含义,而不是添加两个新关键字:
class Foo
def bar
'Hello'
end
end
class Foo
redef bar
super + ' World'
end
end
Foo.new.bar # => 'Hello World'
redef
ining方法等同于在prepend
混入中覆盖该方法。在覆盖方法中,super
的行为类似于此提案中的super
或old
。
UnboundMethod#bind
都会返回一个新的、不同的Method
,因此,无论您是连续两次调用还是从不同的线程同时调用两次,我都没有看到任何冲突产生。 - Jörg W Mittagredef
(稍作语法修改)。 - Ajedi32old
和 redef
?我的 2.0.0 没有它们。啊,很难不去想那些未能进入 Ruby 的 其他竞争性想法包括: - Nakilonrequire
。
Foo
的创建和Foo::bar
的使用时,就需要这样做。因此,您必须对该方法进行猴子补丁。 - Halil Özgür