web-dev-qa-db-ja.com

method_missing gotchas in Ruby

Rubyでmethod_missingメソッドを定義するときに注意すべきことはありますか?継承、例外スロー、パフォーマンス、またはその他の何かから、それほど明白ではない相互作用があるかどうか疑問に思っています。

49
Readonly

やや明白なもの:_respond_to?_を再定義する場合は、常に_method_missing_を再定義します。 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
_
60
James A. Rosen

メソッドが欠落しているメソッドが特定のメソッド名のみを検索している場合、探しているものが見つからない場合はsuperを呼び出すことを忘れないでください。そうすれば、他のメソッドが欠落している場合でも同じことができます。

13
Andrew Grimm

メソッド名を予測できる場合は、method_missingに依存するよりも、動的に宣言する方が適切です。これは、method_missingでパフォーマンスが低下するためです。たとえば、次の構文でデータベースビューにアクセスできるように、データベースハンドルを拡張するとします。

selected_view_rows = @dbh.viewname( :column => value, ... )

データベースハンドルのmethod_missingに依存して、ビューの名前としてメソッド名をデータベースにディスパッチするのではなく、データベース内のすべてのビューを事前に決定し、それらを繰り返し処理して@dbhに「viewname」メソッドを作成できます。 。

11
Pistos

構築 ピストスのポイントmethod_missingは、私が試したすべてのRuby実装を呼び出す通常のメソッドよりも、少なくとも1桁遅いです。彼はmethod_missingへの呼び出しを回避するために可能な場合は予測する権利。

冒険心があれば、Rubyのあまり知られていない Delegator クラスをチェックしてください。

5
James A. Rosen

ジェームズの答えは素晴らしいですが、現代のRuby(1.9+)では、Marc-Andréが言っているように、_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>)
_

したがって、_respond_to_missing?_をオーバーライドするときは、常に_method_missing_を定義してください。

1
Capripot

別の落とし穴:

_method_missing_は、_obj.call_method_とobj.send(:call_method)の間で動作が異なります。基本的に、前者はすべてのプライベートメソッドと未定義のメソッドを見逃しますが、後者はプライベートメソッドを見逃しません。

したがって、誰かがsendを介してプライベートメソッドを呼び出したときに、_method_missing_が呼び出しをトラップすることはありません。

1
jack2684