ChatGPT解决这个技术问题 Extra ChatGPT

当猴子修补实例方法时,您可以从新实现中调用覆盖的方法吗?

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

例如

class Foo
  def bar()
    "Hello"
  end
end 

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

>> Foo.new.bar == "Hello World"
第一个 Foo 类不应该是其他类,而第二个 Foo 不应该继承自它吗?
不,我是猴子补丁。我希望有像 super() 这样的东西可以用来调用原始方法
当您不控制 Foo 的创建和 Foo::bar 的使用时,这是需要的。所以你必须monkey patch这个方法。

B
BenKoshy

编辑:我最初写这个答案已经 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 PatternFactory Method Design Pattern Abstract Factory Design Pattern 或类似的东西,这会更好,因为在这种情况下,只有您需要更改的地方。

代表团

如果您控制 Foo 对象的创建,例如因为它们是由不受您控制的框架创建的(例如 ),那么您可以使用 { 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#prependModule#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 的行为类似于此提案中的 superold


@Jörg W Mittag,方法包装方法线程安全吗?当两个并发线程在同一个 old_method 变量上调用 bind 时会发生什么?
@KandadaBoggu:我试图弄清楚你的意思是什么确切 :-) 但是,我很确定它的线程安全性不亚于Ruby 中任何其他类型的元编程。特别是,每次对 UnboundMethod#bind 的调用都会返回一个新的、不同的 Method,因此,无论您是连续调用两次还是从不同线程同时调用两次,我都没有看到任何冲突。
自从我开始使用 ruby 和 rails 以来,我一直在寻找这样的补丁解释。很好的答案!我唯一缺少的是关于 class_eval 与重新开课的注释。这是:stackoverflow.com/a/10304721/188462
您在哪里找到 oldredef?我的 2.0.0 没有它们。啊,很难不错过 没有进入 Ruby 的其他竞争想法是:
V
Veger

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

有关更多信息和起点,请查看此 replacing methods article(尤其是第一部分)。 Ruby API docs 还提供了(一个不太详细的)示例。


r
rplaurindo

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