Reputation: 355444
Are there any things to be careful about when defining the method_missing
method in Ruby? I'm wondering whether there are some not-so-obvious interactions from inheritance, exception throwing, performance, or anything else.
Upvotes: 51
Views: 20201
Reputation: 1509
James' answer is great but, in modern ruby (1.9+), like Marc-André is saying, you want to redefine respond_to_missing?
because it gives you access to other methods on top of respond_to?
, like method(:method_name)
returning the method itself.
Example, the defined following class:
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
Results in:
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>)
So, always define respond_to_missing?
when overriding method_missing
.
Upvotes: 3
Reputation: 360
Another gotcha:
method_missing
behaves differently between obj.call_method
and obj.send(:call_method)
. Essentially the former one miss all private and non-defined methods, while later one doesn't miss private methods.
So you method_missing
will never trap the call when someone calling your private method via send
.
Upvotes: 1
Reputation: 65232
A somewhat obvious one: always redefine respond_to?
if you redefine method_missing
. If method_missing(:sym)
works, respond_to?(:sym)
should always return true. There are many libraries that rely on this.
Later:
An example:
# 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
Upvotes: 60
Reputation: 81440
If your method missing method is only looking for certain method names, don't forget to call super if you haven't found what you're looking for, so that other method missings can do their thing.
Upvotes: 13
Reputation: 65232
Building on Pistos's point: method_missing
is at least an order of magnitude slower than regular method calling on all the Ruby implementations I've tried. He is right to anticipate when possible to avoid calls to method_missing
.
If you're feeling adventurous, check out Ruby's little-known Delegator class.
Upvotes: 6
Reputation: 23782
If you can anticipate method names, it is better to dynamically declare them than to rely on method_missing because method_missing incurs a performance penalty. For example, suppose you wanted to extend a database handle to be able to access database views with this syntax:
selected_view_rows = @dbh.viewname( :column => value, ... )
Rather than relying on method_missing on the database handle and dispatching the method name to the database as the name of a view, you could determine all the views in the database ahead of time, then iterate over them to create "viewname" methods on @dbh.
Upvotes: 11