Ruby的`method_missing`的3个实际用途值得了解

364 阅读6分钟

Ruby的活力让人敬畏。其中的一个方法是method_missingmethod_missing ,定义在BasicObjectmethod_missing ,让我们可以。

让我们来讨论这个方法的实际用途。

什么是Ruby的method_missing?

我们在类里面定义方法。当一个对象调用一个方法时,Ruby会遍历一个方法的查找路径,从对象的类开始,沿着对象的类的祖先链向上,以到达该方法。如果对象调用的方法在查找路径中是可用的,Ruby就调用它。在某些情况下,该方法不存在于祖先链中。

所有失败的方法查找最终都会到达一个叫做method_missing 的方法。method_missing 位于BasicObject类中,是所有类的根。

对于一个没有混入其他模块或预置其他模块或继承其他类的类,Aircraft ,这个遍历会是这样的(为了简单起见,我省略了通过特征类的查找):

method_missing 用于错误处理

也许method_missing 最常见的用途是它对优雅地处理错误的支持。当一个对象调用一个方法时,Ruby会浏览方法查找链,寻找要执行的方法。当到达BasicObjectmethod_missing 被调用。解释器在这一点上提出了一个错误。

这里有一个例子:

class Aircraft
  attr_accessor :passengers

  def initialize
    @passengers = 0
  end

  def taxi
  end

  def park
  end
end

aircraft = Aircraft.new
aircraft.fly

当你在一个对象上调用一个不存在的方法时,会发生这样的情况。当在继承树中搜索#fly 方法无果而终时,那么调用aircraft.fly 方法将输出<main>: undefined method 'fly' for #<Aircraft:0x00007fe66c1591e0 @passengers=0> (NoMethodError)

如果能够以一种更友好的方式来处理这个错误就更好了。Ruby通过让程序员重写method_missing 来实现这个功能。method_missing 需要三个参数;一个方法名,它的参数列表和一个块。后面两个是可选的。

为了处理这些错误,你用你自己的方法拦截对BasicObject的method_missing的调用。下面是我们重写的Aircraft 代码:

class Aircraft
  attr_accessor :passengers

  def initialize
    @passengers = 0
  end

  def method_missing(method_name, *args)
    message = "You called #{method_name} with #{args}. This method doesn't exist."

    raise NoMethodError, message
  end
end

aircraft = Aircraft.new
aircraft.add_passengers(275)

通过这一改动,我们现在有了'method_missing': You called add_passengers with [275]. This method doesn't exist. (NoMethodError) 作为输出,这看起来比我们之前看到的错误更受欢迎。这个例子显示了Ruby允许程序员拥有自己的代码的程度。 一个稍微复杂的例子可能涉及到super 的使用,如果有人调用一个不存在的方法,你会希望首先对该方法进行一些操作,然后只将控制权传递给原来的method_missing 方法,如Ruby文档中的这个例子所示:

class Roman
  def roman_to_int(str)
    # ...
  end

  def method_missing(symbol, *args)
    str = symbol.id2name
    begin
      roman_to_int(str)
    rescue
      super(symbol, *args)
    end
  end
end

r = Roman.new

r.iv      #=> 4
r.xxiii   #=> 23
r.mm      #=> 2000
r.foo     #=> NoMethodError

随着对method_missing 的使用越来越复杂,责任也越来越重大。当你覆盖method_missing ,你必须确保你在里面调用的任何方法都是可用的。否则,你就会绕圈子,最终得到一个SystemStackError

Aircraft 类的基础上扩展,这里有一些更有意义的东西。如果在一个Aircraft 实例上的方法调用以 "add_"开头,我们就在这个实例上调用public_send ,传递给它一个setter方法,设置 "add_"后面的任何变量:

class Aircraft
  attr_accessor :passengers

  def initialize
    @passengers = 0
  end

  def method_missing(method_name, *args, &block)
    if method_name =~ /add_(.*)/
      public_send("#{Regexp.last_match(1)}=", *args)
    else
      super
    end
  end
end

aircraft = Aircraft.new
aircraft.add_passengers(275)

p aircraft.passengers #=> 275
p aircraft.respond_to?(:add_passengers) #=> false

method_missing 方法中,如果在Aircraft 的任何实例上调用的方法与 "add_"相匹配,我们就会调用一个setter方法,并将方法调用时的参数传递给它。

method_missing然而,如果你注意到respond_to "add_passengers "会返回false,这是不正确的,这就是为什么在任何时候覆盖respond_to_missing? 方法都是好的做法。

在我们的例子中,我们添加:

def respond_to_missing?(method_name, include_private = false)
  method_name =~ /add_(.*)/ || super
end
class Aircraft
  attr_accessor :passengers

  def initialize
    @passengers = 0
  end

  def method_missing(method_name, *args, &block)
    if method_name =~ /add_(.*)/
      public_send("#{Regexp.last_match(1)}=", *args)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name =~ /add_(.*)/ || super
  end
end

aircraft = Aircraft.new
aircraft.add_passengers(275)

p aircraft.passengers #=> 275
p aircraft.respond_to?(:add_passengers) #=> true

方法委托与method_missing

根据维基百科,委托是指在另一个原始对象(发送者)的上下文中评估一个对象(接收者)的成员(属性或方法),这意味着,一个对象正在要求另一个对象执行接收者正在调用的方法中的动作。

你可以使用Ruby的method_missing来将方法委托给另一个类。

这里有一个假想的例子:

class Referee
  def red_card?
    puts "true"
  end
end


class Linesman
  def initialize(referee)
    @referee = referee
  end

  def method_missing(method, *args)
    @referee.send(method)
  end

end

linesman = Linesman.new(Referee.new)

linesman.red_card? # => true

在上面的代码中,我们在linesman ,一个Linesman 的实例上调用red_card? ,但是是一个Referee 的实例来完成工作,这是某种形式的委托的例子。

Ruby在其标准库中提供了一个叫做delegate的库,如果你需要进行严肃的方法委托,你可能想先看看。

使用method_missing构建DSL和库

特定领域语言(DSL)是一种可以完成特定任务的语言。与Ruby不同,像RSpec这样的DSL只对测试有用。

DSL相对来说更容易编写。DSL无处不在。几个例子是:

  • Capistrano
  • Rake
  • Sinatra
  • Rails 路由
  • factory_bot

Ruby的method_missing使构建DSL变得非常容易。让我们用最简单的XML生成器来试试吧。这是我从《Ruby编程语言》一书中抓取的一个修改过的片段:

class XML
  class XML
  def initialize(output)
    @output = output
  end

  def method_missing(tag, attributes = {})
    @output << "<#{tag}"

    if block_given?
      @output << '>'
      content = yield

      @output << content.to_s if content

      @output << "</#{tag}>"
    else
      @output << '/>'
    end
    nil
  end

  def self.generate(output, &block)
    XML.new(output).instance_eval(&block)
  end
end

这段代码允许我们写出这样的东西:

  XML.generate(STDOUT) do
  html do
    head do
      title "Page Title"
    end
    body do
     strong do
      "Strong Text"
     end
    end
  end
end

运行这个生成器:

<html><head><title/></head><body><strong>Strong Text</strong></body></html>

就这样,我们利用了method_missing 的力量,创建了一个生成XML的DSL。

依靠method_missing 的DSL的一个优秀例子是The Late Jim Weirich的Buildergem。Builder gem从块中生成XML标记,我们刚才写的很像,但更复杂。

另一个看起来很熟悉的例子是Ruby on Rails依赖 method_missing 来创建动态查找器,如ActiveRecord中的find_by_* 组方法。

关于method_missing的遍历问题

我昨天从一个小组了解到,Ruby对method_missing 进行了两次遍历。第一次是为了到达BasicObject 中的method_missing ,第二次遍历是为了检查你是否覆盖了method_missing :这是有道理的,否则,Ruby怎么会捡到你覆盖的method_missing

我没有找到这方面的任何参考资料。如果你有一些关于这个事实的文档,请分享。

总结

我们已经看到如何使用method_missing 来处理错误和进行方法委托。在另一个元编程结构的帮助下,我们看到了如何使用instance_eval 来编写一个生成XML的DSL。我们看了Ruby如何遍历祖先链来寻找方法,并在没有找到时调用method_missingrespond_to_missing? 我们了解到,在任何时候覆盖method_missing? ,都是一个好主意;这涵盖了你可以用method_missing 做的事情的清单。 如果你能想到任何其他的用例,请在下面的评论中分享。