Ruby中method_missing的陷阱

51

在定义 Ruby 中的 method_missing 方法时,有哪些需要注意的事项?我想知道是否存在一些来自继承、异常抛出、性能或其他方面的不太明显的相互作用。

6个回答

60
一个比较明显的问题是:如果重新定义了method_missing,就必须重新定义respond_to?。如果method_missing(:sym)有效,则respond_to?(:sym)应始终返回true。有许多库依赖于此。
后面是一个示例:
# Wrap a Foo; don't expose the internal guts.
# Pass any method that starts with 'a' on to the
# Foo.
class FooWrapper
  def initialize(foo)
    @foo = foo
  end
  def some_method_that_doesnt_start_with_a
    'bar'
  end
  def a_method_that_does_start_with_a
    'baz'
  end
  def respond_to?(sym, include_private = false)
    pass_sym_to_foo?(sym) || super(sym, include_private)
  end
  def method_missing(sym, *args, &block)
    return foo.call(sym, *args, &block) if pass_sym_to_foo?(sym)
    super(sym, *args, &block)
  end
  private
  def pass_sym_to_foo?(sym)
    sym.to_s =~ /^a/ && @foo.respond_to?(sym)
  end
end

class Foo
  def argh
    'argh'
  end
  def blech
    'blech'
  end
end

w = FooWrapper.new(Foo.new)

w.respond_to?(:some_method_that_doesnt_start_with_a)
# => true
w.some_method_that_doesnt_start_with_a
# => 'bar'

w.respond_to?(:a_method_that_does_start_with_a)
# => true
w.a_method_that_does_start_with_a
# => 'baz'

w.respond_to?(:argh)
# => true
w.argh
# => 'argh'

w.respond_to?(:blech)
# => false
w.blech
# NoMethodError

w.respond_to?(:glem!)
# => false
w.glem!
# NoMethodError

w.respond_to?(:apples?)
w.apples?
# NoMethodError

很有趣。你会如何实现一个既包含“普通”方法又包含通过 method_missing 实现的 “动态” 方法的类? - Christoph Schiessl
@Christoph:你的 pass_sym_to_foo? 方法变成了一个通用的 handle? 方法,它决定是尝试处理这个请求还是将其交给 supermethod_missing - John Feminella
16
在Ruby 1.9.2中,重新定义respond_to_missing?方法效果更佳,可参考我的博客文章:http://blog.marc-andre.ca/2010/11/methodmissing-politely.html。 - Marc-André Lafortune
2
这里需要做一些更正: 1)respond_to? 实际上需要两个参数。如果未指定第二个参数,可能会导致难以察觉的参数错误(请参见 http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/ ) 2)在这种情况下,您无需向super传递参数。 super 隐式地使用原始参数调用超类方法。 - Cory Schires

13
如果您的method_missing方法只寻找特定的方法名称,请不要忘记在未找到您要寻找的内容时调用super,以便其他method_missing方法可以执行它们的操作。

1
是的 - 否则您的方法调用将悄无声息地失败,您将花费数小时试图弄清楚为什么您的方法不起作用,尽管没有错误。(并不是我会这样做) - PhillipKregg

11
如果您可以预测方法名称,最好动态声明它们,而不要依赖method_missing,因为method_missing会导致性能损失。例如,假设您想扩展数据库句柄以便使用此语法访问数据库视图:
selected_view_rows = @dbh.viewname( :column => value, ... )

与其依靠数据库句柄上的method_missing,并将方法名作为视图名称分派到数据库中,您可以事先确定数据库中所有视图,然后对它们进行迭代,以在@dbh上创建"视图名称"方法。


6

基于Pistos的观点:在我尝试过的所有Ruby实现中,method_missing至少比常规方法调用慢一个数量级。他正确地预测了尽可能避免调用method_missing

如果你感到冒险,请查看Ruby中鲜为人知的Delegator类。


3

詹姆斯的答案很好,但在现代 Ruby(1.9+)中,就像马克 - 安德烈所说的那样,您需要重新定义 respond_to_missing?,因为它可以让您访问其他方法,例如respond_to?之上的method(:method_name)返回方法本身。

例如,下面定义了一个类:

class UserWrapper
  def initialize
    @json_user = { first_name: 'Jean', last_name: 'Dupont' }
  end

  def method_missing(sym, *args, &block)
    return @json_user[sym] if @json_user.keys.include?(sym)
    super
  end

  def respond_to_missing?(sym, include_private = false)
    @json_user.keys.include?(sym) || super
  end
end

结果是:

irb(main):015:0> u = UserWrapper.new
=> #<UserWrapper:0x00007fac7b0d3c28 @json_user={:first_name=>"Jean", :last_name=>"Dupont"}>
irb(main):016:0> u.first_name
=> "Jean"
irb(main):017:0> u.respond_to?(:first_name)
=> true
irb(main):018:0> u.method(:first_name)
=> #<Method: UserWrapper#first_name>
irb(main):019:0> u.foo
NoMethodError (undefined method `foo' for #<UserWrapper:0x00007fac7b0d3c28>)

因此,在覆盖 method_missing 时,始终定义respond_to_missing?

1

还有一个需要注意的地方:

method_missingobj.call_methodobj.send(:call_method) 之间的行为是不同的。实际上,前者会忽略所有私有方法和未定义的方法,而后者则不会忽略私有方法。

因此,当有人通过 send 调用您的私有方法时,您的 method_missing 永远不会捕获到这个调用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接