Ruby的活力让人敬畏。其中的一个方法是method_missing。method_missing ,定义在BasicObject 。method_missing ,让我们可以。
让我们来讨论这个方法的实际用途。
什么是Ruby的method_missing?
我们在类里面定义方法。当一个对象调用一个方法时,Ruby会遍历一个方法的查找路径,从对象的类开始,沿着对象的类的祖先链向上,以到达该方法。如果对象调用的方法在查找路径中是可用的,Ruby就调用它。在某些情况下,该方法不存在于祖先链中。
所有失败的方法查找最终都会到达一个叫做method_missing 的方法。method_missing 位于BasicObject类中,是所有类的根。
对于一个没有混入其他模块或预置其他模块或继承其他类的类,Aircraft ,这个遍历会是这样的(为了简单起见,我省略了通过特征类的查找):

method_missing 用于错误处理
也许method_missing 最常见的用途是它对优雅地处理错误的支持。当一个对象调用一个方法时,Ruby会浏览方法查找链,寻找要执行的方法。当到达BasicObject ,method_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_missing 。respond_to_missing? 我们了解到,在任何时候覆盖method_missing? ,都是一个好主意;这涵盖了你可以用method_missing 做的事情的清单。 如果你能想到任何其他的用例,请在下面的评论中分享。