假设我正在修补类中的方法,我怎么能从覆盖方法中调用被覆盖的方法?即有点像super
例如
class Foo
def bar()
"Hello"
end
end
class Foo
def bar()
super() + " World"
end
end
>> Foo.new.bar == "Hello World"
编辑:我最初写这个答案已经 9 年了,它值得做一些整容手术来保持它的最新状态。
您可以看到修改前的最后一个版本 here。
您不能通过名称或关键字调用覆盖的方法。这就是为什么应该避免猴子修补而首选继承的众多原因之一,因为显然您可以调用被覆盖的方法。
避免猴子修补
遗产
所以,如果可能的话,你应该更喜欢这样的东西:
class Foo
def bar
'Hello'
end
end
class ExtendedFoo < Foo
def bar
super + ' World'
end
end
ExtendedFoo.new.bar # => 'Hello World'
如果您控制 Foo
对象的创建,则此方法有效。只需将创建 Foo
的每个位置更改为创建 ExtendedFoo
。如果您使用 Dependency Injection Design Pattern、Factory Method Design Pattern、 Abstract Factory Design Pattern 或类似的东西,这会更好,因为在这种情况下,只有您需要更改的地方。
代表团
如果您不控制 Foo
对象的创建,例如因为它们是由不受您控制的框架创建的(例如 ruby-on-rails),那么您可以使用 { 2}:
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
对象进入您的代码的位置,您将其包装到另一个对象中,然后在代码中的其他任何地方使用 that 对象而不是原始对象。
这使用了标准库中 delegate
库中的 Object#DelegateClass
辅助方法。
“清洁”猴子补丁
Module#prepend: Mixin 前置
上述两种方法都需要更改系统以避免猴子补丁。本节展示了猴子修补的首选且侵入性最小的方法,如果不能选择更改系统。
添加了 Module#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 module prepend vs derivation
Mixin 继承(损坏)
我见过一些人尝试(并询问为什么它在 StackOverflow 上不起作用)这样的事情,即 include
使用 mixin 而不是 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)
这有点棘手。基本上,在 Ruby(以及几乎所有基于单调度的 OO 语言中)中,方法绑定到特定的接收器对象,在 Ruby 中称为 self
。换句话说:一个方法总是知道它被调用的是什么对象,它知道它的 self
是什么。但是,我们直接从一个类中获取方法,它怎么知道它的 self
是什么?
好吧,它没有,这就是为什么我们需要先将 bind
我们的 UnboundMethod
指向一个对象,这将返回一个 Method
对象,然后我们可以调用该对象。 (不能调用 UnboundMethod
,因为他们不知道自己的 self
不知道该做什么。)
我们bind
它做什么?我们只需将其bind
分配给自己,这样它的行为就会完全 像原来的 bar
一样!
最后,我们需要调用从 bind
返回的 Method
。在 Ruby 1.9 中,有一些漂亮的新语法 (.()
),但如果您使用的是 1.8,则可以简单地使用 call
方法;无论如何,这就是 .()
被翻译的内容。
以下是其他几个问题,其中解释了其中一些概念:
如何在 Ruby 中引用函数?
Ruby 的代码块和 C♯ 的 lambda 表达式一样吗?
“肮脏”的猴子补丁
别名方法链
我们在猴子补丁中遇到的问题是,当我们覆盖该方法时,该方法就消失了,所以我们不能再调用它了。所以,让我们做一个备份副本!
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 的代码完成中,它会出现在反射过程中。此外,它仍然可以调用,但大概我们猴子修补了它,因为我们一开始不喜欢它的行为,所以我们可能不希望其他人调用它。
尽管它有一些不受欢迎的特性,但不幸的是,它已通过 AciveSupport 的 Module#alias_method_chain
普及。
旁白:改进
如果您只需要在几个特定位置而不是整个系统中的不同行为,您可以使用 Refinements 将猴子补丁限制在特定范围内。我将在此处使用上面的 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!
您可以在这个问题中看到一个使用 Refinements 的更复杂的示例:How to enable monkey patch for specific method?
放弃的想法
在 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
的返回值还不是很清楚。也许我们可以(ab)使用 super
关键字?
class Foo
def bar
'Hello'
end
end
class Foo
def bar:after
super + ' World'
end
end
替代品
before 组合器相当于prepend
使用在方法的最 end 处调用 super
的覆盖方法的 mixin。同样,after 组合符相当于prepend
使用在方法的最开头调用 super
的覆盖方法使用 mixin。
还可以在调用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
旧关键字
这个想法添加了一个类似于 super
的新关键字,它允许您调用 overwritten 方法,就像 super
允许您调用 overridden 方法一样:
class Foo
def bar
'Hello'
end
end
class Foo
def bar
old + ' World'
end
end
Foo.new.bar # => 'Hello World'
这样做的主要问题是它向后不兼容:如果您有名为 old
的方法,您将无法再调用它!
替代品
prepend
ed mixin 中的覆盖方法中的 super
与此提案中的 old
基本相同。
重定义关键字
与上面类似,但我们不是为调用被覆盖的方法添加一个新关键字并单独留下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
引入方法等同于覆盖prepend
混合中的方法。覆盖方法中的 super
的行为类似于此提案中的 super
或 old
。
看看别名方法,这是将方法重命名为新名称。
有关更多信息和起点,请查看此 replacing methods article(尤其是第一部分)。 Ruby API docs 还提供了(一个不太详细的)示例。
将进行覆盖的类必须在包含原始方法的类之后重新加载,因此require
将其放在将进行覆盖的文件中。
old_method
变量上调用bind
时会发生什么?UnboundMethod#bind
的调用都会返回一个新的、不同的Method
,因此,无论您是连续调用两次还是从不同线程同时调用两次,我都没有看到任何冲突。old
和redef
?我的 2.0.0 没有它们。啊,很难不错过 没有进入 Ruby 的其他竞争想法是: