method_missing gotchas in Ruby

前端 未结 6 769
天命终不由人
天命终不由人 2020-12-22 23:07

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 fro

相关标签:
6条回答
  • 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
    
    0 讨论(0)
  • 2020-12-22 23:36

    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.

    0 讨论(0)
  • 2020-12-22 23:43

    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.

    0 讨论(0)
  • 2020-12-22 23:47

    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.

    0 讨论(0)
  • 2020-12-22 23:52

    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.

    0 讨论(0)
  • 2020-12-22 23:58

    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.

    0 讨论(0)
提交回复
热议问题