478

假设我正在修补类中的方法,我怎么能从覆盖方法中调用被覆盖的方法?即有点像super

例如

class Foo
  def bar()
    "Hello"
  end
end 

class Foo
  def bar()
    super() + " World"
  end
end

>> Foo.new.bar == "Hello World"
4

3 回答 3

1229

编辑:我最初写这个答案已经 9 年了,它值得做一些整容手术来保持它的最新状态。

您可以在此处查看编辑前的最后一个版本。


您不能通过名称或关键字调用覆盖的方法。这就是为什么应该避免猴子修补而首选继承的众多原因之一,因为显然您可以调用被覆盖的方法。

避免猴子修补

遗产

所以,如果可能的话,你应该更喜欢这样的东西:

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

ExtendedFoo.new.bar # => 'Hello World'

如果您控制Foo对象的创建,则此方法有效。只需将创建 a 的每个地方更改Foo为创建ExtendedFoo. 如果你使用依赖注入设计模式工厂方法设计模式抽象工厂设计模式或类似的东西,这会更好,因为在这种情况下,只有你需要改变的地方。

代表团

如果您控制Foo对象的创建,例如因为它们是由您无法控制的框架创建的(例如),那么您可以使用Wrapper Design Pattern

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对象进入您的代码的地方,您将其包装到另一个对象中,然后在代码中的其他任何地方使用该对象而不是原始对象。

这使用了 stdlib 中库中的Object#DelegateClass辅助方法。delegate

“清洁”猴子补丁

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 而不是prepending 它:

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)

在这里,我们将方法包装barUnboundMethod方法对象中并将其分配给局部变量old_barbar这意味着,即使在它被覆盖之后,我们现在也有办法坚持下去。

old_bar.bind(self)

这有点棘手。基本上,在 Ruby(以及几乎所有基于单调度的 OO 语言中)中,方法绑定到特定的接收器对象,self在 Ruby 中调用。换句话说:一个方法总是知道它被调用的对象是什么,它知道它self是什么。但是,我们直接从一个类中获取方法,它怎么知道它self是什么?

好吧,它没有,这就是为什么我们需要bind先到UnboundMethod一个对象,这将返回一个Method我们可以调用的对象。(UnboundMethods 不能被调用,因为他们不知道他们不知道该怎么做self。)

我们bind有什么用?我们只是bind给自己做,这样它的行为就和原来的完全bar一样!

最后,我们需要调用Methodbind. 在 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 的代码完成中,它会出现在反射过程中。此外,它仍然可以调用,但大概我们猴子修补了它,因为我们一开始不喜欢它的行为,所以我们可能不希望其他人调用它。

尽管它有一些不受欢迎的特性,但不幸的是它通过 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 社区决定使用 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方法的执行。

但是,您是否以及如何barbar:after. 也许我们可以(ab)使用super关键字?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

替代品

before 组合符相当于使用在方法的最后prepend调用的覆盖方法的 mixin 。同样, after 组合符等同于使用在方法的最开始调用的覆盖方法的 mixin 。superprependsuper

你也可以在调用之前之后做一些事情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你调用被覆盖的方法一样:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

这样做的主要问题是它向后不兼容:如果你有方法被调用old,你将无法再调用它!

替代品

superprepended mixin 中的覆盖方法中与此提案中的基本相同old

redef关键词

与上面类似,但我们不是添加一个新关键字来调用被覆盖的方法并不管def不顾,而是添加一个新关键字来重新定义方法。这是向后兼容的,因为目前的语法无论如何都是非法的:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

除了添加两个super新关键字,我们还可以重新定义inside的含义redef

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

替代品

redefining 方法等同于在prepended mixin 中覆盖方法。super在覆盖方法中的行为类似于superold在此提案中。

于 2010-12-17T14:08:43.027 回答
13

看看别名方法,这是将方法重命名为新名称。

有关更多信息和起点,请查看这篇替换方法文章(尤其是第一部分)。Ruby API docs也提供了(一个不太详细的)示例。

于 2010-12-17T11:52:50.403 回答
-2

将进行覆盖的类必须在包含原始方法的类之后重新加载,因此require它在将进行覆盖的文件中。

于 2016-04-19T04:39:53.333 回答