【问题标题】:Overriding method by another defined in module由模块中定义的另一个覆盖方法
【发布时间】:2011-08-22 02:30:26
【问题描述】:

我想定义一个实例方法Date#next,它会在第二天返回。所以我做了一个DateExtension 模块,像这样:

module DateExtension
  def next(symb=:day)
    dt = DateTime.now
    {:day   => Date.new(dt.year, dt.month, dt.day + 1),
     :week  => Date.new(dt.year, dt.month, dt.day + 7),
     :month => Date.new(dt.year, dt.month + 1, dt.day),
     :year  => Date.new(dt.year + 1, dt.month, dt.day)}[symb]
  end
end

使用它:

class Date
  include DateExtension
end

调用d.next(:week) 方法会使Ruby 抛出错误ArgumentError: wrong number of arguments (1 for 0)。 如何使用 DateExtension 模块中声明的方法覆盖 Date 类中的默认 next 方法?

【问题讨论】:

标签: ruby methods module


【解决方案1】:

在 Ruby 2.0 及更高版本中,您可以使用Module#prepend

class Date
  prepend DateExtension
end

下面是旧 Ruby 版本的原始答案。


include 的问题(如the following diagram 所示)是一个类的方法不能被该类中包含的模块覆盖(解决方案如下图):

解决方案

  1. 仅针对这一方法的子类日期:

    irb(main):001:0> require 'date'; module Foo; def next(a=:hi); a; end; end
    #=> nil
    irb(main):002:0> class MyDate < Date; include Foo; end
    #=> MyDate
    irb(main):003:0> MyDate.today.next(:world)
    #=> :world
    
  2. 使用您自己的方法仅扩展您需要的实例:

    irb(main):001:0> require 'date'; module Foo; def next(a=:hi); a; end; end
    #=> nil
    irb(main):002:0> d = Date.today; d.extend(Foo); d.next(:world)
    #=> :world
    
  3. 当包含你的模块时,执行一个粗略的 hack 并进入类并销毁旧的“下一个”,以便调用你的模块:

    irb(main):001:0> require 'date'
    #=> true
    irb(main):002:0> module Foo
    irb(main):003:1>   def self.included(klass)
    irb(main):004:2>     klass.class_eval do
    irb(main):005:3*       remove_method :next
    irb(main):006:3>     end
    irb(main):007:2>   end
    irb(main):008:1>   def next(a=:hi); a; end
    irb(main):009:1> end
    #=> nil
    irb(main):010:0> class Date; include Foo; end
    #=> Date
    irb(main):011:0> Date.today.next(:world)
    #=> :world
    

    这种方法比仅仅包含一个模块更具侵入性,但唯一的方法(到目前为止显示的技术)使得系统方法返回的新 Date 实例将自动使用您自己模块中的方法。

  4. 但如果你打算这样做,你不妨完全跳过该模块,直接进入monkeypatch土地:

    irb(main):001:0> require 'date'
    #=> true
    irb(main):002:0> class Date
    irb(main):003:1>   alias_method :_real_next, :next
    irb(main):004:1>   def next(a=:hi); a; end
    irb(main):005:1> end
    #=> nil
    irb(main):006:0> Date.today.next(:world)
    #=> :world
    
  5. 如果您在自己的环境中确实需要此功能,请注意,banisterfiend 的 Prepend 库可以让您在模块中进行查找,然后再将其混入其中。

【讨论】:

  • RUBY_DESCRIPTION => "ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]" 我刚刚意识到一些事情。我的代码使用的是 DateTime 类,所以我需要require 'date'(根据this)。在此指令之后使用您的示例,Ruby 会引发错误。我发布了gist,因为我无法在此处发布整个代码。谢谢!
  • @kaos12 谢谢,我已经用各种可用的选项编辑了我的答案。
  • @kaos12 行为没有变化。包含模块中的方法总是“高于”它们包含的类。试试这个:class MyClass; end; [Object,Class,Module,MyClass].each{ |k| k.class_eval{ define_method(:foo){ puts "#{k}#foo"; super() unless k==Object }}}; MyClass.new.foo 没有包含模块的类上的实例方法直接转到Object 上的实例方法。
  • 未来,Module#prepend 可能值得研究。
  • Module#prepend 已经在 Ruby 2.0 中可用
【解决方案2】:

Datenext 方法在 Date 类中定义,并且在类中定义的方法优先于在包含模块中定义的方法。所以,当你这样做时:

class Date
  include DateExtension
end

您正在引入您的next 版本,但Date 中定义的next 仍然优先。你必须把你的next 放在Date

class Date
  def next(symb=:day)
    dt = DateTime.now
      {:day   => Date.new(dt.year, dt.month, dt.day + 1),
       :week  => Date.new(dt.year, dt.month, dt.day + 7),
       :month => Date.new(dt.year, dt.month + 1, dt.day),
       :year  => Date.new(dt.year + 1, dt.month, dt.day)}[symb]
    end
end

来自 Classes and Objects 的 Programming Ruby 章节:

当一个类包含一个模块时,该模块的实例方法可用作该类的实例方法。就好像模块成为使用它的类的超类一样。毫不奇怪,这就是它的工作原理。当您包含一个模块时,Ruby 会创建一个引用该模块的匿名代理类,并将该代理插入为执行包含的类的直接超类。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-03-28
    • 2017-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多