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

2023-11-25

假设我正在修补类中的方法,我如何从重写方法中调用重写方法? IE。有点像super

E.g.

class Foo
  def bar()
    "Hello"
  end
end 

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

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

EDIT:距离我最初写这个答案已经过去了 9 年,值得做一些整容手术以使其保持最新状态。

可以看到修改前的最新版本here.


您不能调用被覆盖按名称或关键字的方法。这是应该避免猴子修补并首选继承的众多原因之一,因为显然你can打电话给被覆盖 method.

避免猴子修补

遗产

因此,如果可能的话,您应该更喜欢这样的东西:

class Foo
  def bar
    'Hello'
  end
end 

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

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

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

代表团

If you do not控制创建Foo对象,例如因为它们是由您无法控制的框架创建的(例如轨道上的红宝石例如),那么你可以使用包装设计模式:

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对象而不是代码中其他地方的原始对象。

这使用了Object#DelegateClass辅助方法来自delegatestdlib 中的库。

“干净”的猴子补丁

Module#prepend: 混合前置

上述两种方法都需要更改系统以避免猴子补丁。本节展示了猴子修补的首选且侵入性最小的方法,如果无法更改系统的话。

Module#prepend被添加来或多或少地支持这个用例。Module#prepend做同样的事情Module#include,除了它直接混合在 mixin 中below班上:

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 模块前置与派生

Mixin 继承(已损坏)

我见过一些人尝试(并询问为什么它在 StackOverflow 上不起作用)这样的事情,即include使用 mixin 代替prepending it:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

不幸的是,这行不通。这是一个好主意,因为它使用继承,这意味着您可以使用super。然而,Module#include插入 mixinabove继承层次结构中的类,这意味着FooExtensions#bar永远不会被调用(如果它were叫做super实际上不会指Foo#bar而是为了Object#bar不存在),因为Foo#bar总是会先被找到。

方法包装

最大的问题是:我们如何才能坚持下去bar方法,而不实际保留实际方法?正如经常出现的那样,答案就在于函数式编程。我们掌握了该方法作为实际的方法object,并且我们使用闭包(即块)来确保我们并且只有我们抓住那个物体:

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只是一个局部变量,它会在类体末尾超出范围,并且无法从任何地方访问它,even使用反射!自从Module#define_method占用一个块,并且块靠近其周围的词汇环境(即why我们正在使用define_method代替def here), it (and only它)仍然可以访问old_bar,即使它超出了范围。

简短说明:

old_bar = instance_method(:bar)

在这里我们正在包装bar方法转化为UnboundMethod方法对象并将其分配给局部变量old_bar。这意味着,我们现在有办法坚持下去bar即使它已被覆盖。

old_bar.bind(self)

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

嗯,事实并非如此,这就是为什么我们需要bind our UnboundMethod首先到一个对象,这将返回一个Method我们可以调用的对象。 (UnboundMethods 无法被调用,因为他们不知道在不知道自己的情况下该怎么做self.)

而我们该怎么办bind它到?我们简单地bind它对我们自己来说,它就会以这种方式表现exactly像原来的一样bar将有!

最后,我们需要调用Method这是从返回的bind。在 Ruby 1.9 中,有一些漂亮的新语法(.()),但如果你使用的是 1.8,你可以简单地使用call方法;就是这样.()无论如何都会被翻译成。

以下是其他几个问题,其中解释了其中一些概念:

  • 如何在 Ruby 中引用函数?
  • Ruby 的代码块与 C♯ 的 lambda 表达式相同吗?

“肮脏的”猴子补丁

alias_method chain

我们在猴子修补中遇到的问题是,当我们覆盖该方法时,该方法就消失了,因此我们无法再调用它。所以,让我们制作一个备份副本!

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 的更复杂的示例:如何为特定方法启用猴子补丁?


被放弃的想法

在 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 method.

然而,尚不清楚您是否以及如何访问bar的返回值在bar:after。也许我们可以(滥用)使用super关键词?

class Foo
  def bar
    'Hello'
  end
end 

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

替代品

之前的组合器相当于prepend使用重写方法调用 mixinsuper在非常end该方法的。同样,后组合器相当于prepend使用重写方法调用 mixinsuper在非常开始该方法的。

你也可以先做一些事and打电话后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

and

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 keyword

这个想法添加了一个新的关键字,类似于super,这允许您调用被覆盖方法同样的方法super让你打电话被覆盖 method:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

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

替代品

super在重写方法中prepended mixin 本质上与old在此提案中。

redef keyword

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

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

而不是添加two新的关键词,我们也可以重新定义它的含义super inside redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

替代品

redef调用一个方法相当于重写一个方法prepend编辑混合。super在重写方法中的行为类似于super or old在此提案中。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当猴子修补实例方法时,您可以从新实现中调用重写的方法吗? 的相关文章

随机推荐

  • WaitHandle.WaitAny 和 Semaphore 类

    Edit 我想说这个问题只是暂时的精神错乱 但当时这是有道理的 见下面的编辑2 对于 NET 3 5 项目 我有两种类型的资源 R1 and R2 我需要检查其可用性 每种资源类型在任何时候都可以有 比如说 10 个实例 当任一类型的资源可
  • 如何访问 MIPS 中字的各个位的状态?

    我正在编写一个程序 我需要确定是否设置了位 3 和 6 我知道我可以旋转一个单词或左 右移动它 但如何访问各个位的状态呢 我是否使用像 and xor 这样的按位运算符 您可以使用 0x08 和 0x40 进行按位与运算 假设位 0 是最低
  • 如何枚举私有 JavaScript 类字段

    我们如何通过私有类字段进行枚举 class Person isFoo true isBar false constructor first last this firstName first this lastName last enume
  • Fetch Api 无法从 PHP 服务器获取会话

    我在我的应用程序中使用 Fetch Api 我有一个 PHP 服务器页面来获取之前已经定义的会话数据 看起来像这样
  • NHibernate中的inverse和cascade是什么意思

    我正在学习 Fluent Nhibernate 我的问题是 什么是Inverse意思是 我读到这意味着关系的另一方负责储蓄 也是如此Cascade 有人可以解释一下它们之间有什么区别吗 请详细解释一下 因为我是NH的新手 看一下本文 链接已
  • 如何并行处理 Flux 事件?

    我有需要丰富的传入事件流 然后在它们到达时并行处理 我以为 Project Reactor 是为这项工作定制的 但在我的测试中 所有处理似乎都是串行完成的 这是一些测试代码 ExecutorService executor Executor
  • 二维数组中每个对角线的最大值

    我有数组 需要动态窗口的最大滚动差异 a np array 8 18 5 15 12 print a 8 18 5 15 12 所以首先我自己创造差异 b a a None print b 0 10 3 7 4 10 0 13 3 6 3
  • 如何使用 One Signal + PHP + Server API 发送推送通知?

    我正在使用一个信号来发送 Android 应用程序的推送通知 我的问题是 如何设置使用服务器 REST API 发送推送通知
  • 将 GroupBy 平均结果添加为 pandas 中的新列

    我有一个数据框 给出每个指标的上限和下限值 如下所示 df pd DataFrame indicator indicator 1 indicator 1 indicator 2 indicator 2 year 2014 2014 2015
  • 如何将 jar 放在 jetty 类路径上的 jetty/lib 中?

    我有 Jetty jetty 9 2 3 v20140905 我的理解是 lib jar 或 lib ext 中的 jar 自动位于类路径上 但这可能是 jetty 8 的旧行为 我正在尝试使用 websockets 部署一个 web 应用
  • 自定义 ng-options 选择外观

    我正在使用 ng options 作为选择下拉菜单 我想根据条件使用不同的颜色作为选项 select ng model myCompany ng options company code as company name for compan
  • JQuery 无法与 Vuejs 一起使用

    我正在尝试将 JQuery 插件 owl carousel 添加到使用 Vuejs 渲染的列表中 HTML h4 1 Vuejs rendered items with OWL Carousel not working h4 div cla
  • Ruby 将上下文分配给 lambda?

    是否可以不将上下文分配给 lambda 例如 class Rule def get rule return lambda puts name end end class Person attr accessor name def init
  • 未找到合适的驱动程序 Postgres JDBC

    当我在 tomcat 上测试 Web 服务时 收到 找不到合适的驱动程序 错误 我在 lib 文件夹中有 JDBC jar 正如各种教程所说的那样 这是我的代码 public class PostDBConnection PreparedS
  • 如何合并两个元组列表?

    我在 Scala 中有两个列表 如何合并它们以使元组分组在一起 是否有现有的 Scala 列表 API 可以做到这一点 或者需要我自己做 Input List a 4 b 1 c 1 d 1 List a 1 b 1 c 1 预期输出 Li
  • 从私有 PyPI 定义 setup.py 依赖项

    我想通过在我的私有 PyPI 中指定它们来安装依赖项setup py 我已经尝试指定在何处查找依赖项dependency links这边走 setup install requires foo 1 0 dependency links ht
  • 如何在 R 中为 is.holiday() chron 包定义假期

    我正在尝试使用chron s is holiday 功能 但我无法让它工作 文档说要修改 Holiday反对您想要使用的假期 但我所做的更改 Holiday似乎没有被检测到is holiday 有人可以提供加载假期的适当方法的示例吗 这一点
  • web.config 和 connectionStrings 中的引号

    我有以下连接字符串 您会注意到 Provider s Tests 注意单引号 如何将其输入到 web config 中以使其有效
  • Swift 3.1 弃用了initialize()。我怎样才能实现同样的目标?

    Objective C 声明一个类函数 initialize 在使用之前为每个类运行一次 它通常用作交换方法实现 swizzling 等的入口点 Swift 3 1 弃用了此函数并发出警告 方法 initialize 定义了 Objecti
  • 当猴子修补实例方法时,您可以从新实现中调用重写的方法吗?

    假设我正在修补类中的方法 我如何从重写方法中调用重写方法 IE 有点像super E g class Foo def bar Hello end end class Foo def bar super World end end gt gt