【问题标题】:method_missing gotchas in RubyRuby 中的 method_missing 陷阱
【发布时间】:2010-09-22 09:53:01
【问题描述】:

在 Ruby 中定义method_missing 方法时有什么需要注意的吗?我想知道继承、异常抛出、性能或其他方面是否存在一些不那么明显的交互。

【问题讨论】:

    标签: ruby metaprogramming


    【解决方案1】:

    一个有点明显的:如果你重新定义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:你的pass_sym_to_foo? 方法变成了一个通用的handle? 方法,它决定是尝试处理这个请求还是把它交给supermethod_missing
    • 在 Ruby 1.9.2 中,重新定义 respond_to_missing? 会更好,见我的博文:blog.marc-andre.ca/2010/11/methodmissing-politely.html
    • 这里应该做一些更正: 1) respond_to? 实际上有两个参数。未能指定第二个参数可能会导致细微的参数错误(请参阅technicalpickles.com/posts/…) 2) 在这种情况下,您不需要将参数传递给 super。 super 使用原始参数隐式调用超类方法
    【解决方案2】:

    如果您的方法缺失方法只是查找某些方法名称,如果您没有找到您要查找的内容,请不要忘记调用 super,以便其他缺失的方法可以做他们的事情。

    【讨论】:

    • 是的 - 否则您的方法调用将静默失败,即使没有错误,您也会花费数小时试图找出您的方法无法正常工作的原因。 (不是说我会做这种事)
    【解决方案3】:

    如果您可以预测方法名称,最好动态声明它们而不是依赖 method_missing,因为 method_missing 会导致性能损失。例如,假设您想扩展数据库句柄以便能够使用以下语法访问数据库视图:

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

    您可以提前确定数据库中的所有视图,然后遍历它们以创建“视图名称”方法,而不是依赖于数据库句柄上的 method_missing 并将方法名称作为视图名称分派给数据库在@dbh 上。

    【讨论】:

      【解决方案4】:

      Pistos's point 为基础:method_missing 至少比调用我尝试过的所有 Ruby 实现的常规方法慢一个数量级。他在可能的情况下避免致电method_missing 是正确的。

      如果您喜欢冒险,请查看 Ruby 鲜为人知的 Delegator 课程。

      【讨论】:

        【解决方案5】:

        James 的回答很棒,但是在现代 ruby​​ (1.9+) 中,就像 Marc-André 所说,您想重新定义 respond_to_missing?,因为它使您可以访问 respond_to? 之上的其他方法,例如 @987654323 @return 方法本身。

        例如,定义如下类:

        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?

        【讨论】:

          【解决方案6】:

          另一个问题:

          method_missingobj.call_methodobj.send(:call_method) 之间的行为不同。本质上,前者错过了所有私有和未定义的方法,而后者则不会错过私有方法。

          所以当有人通过send 调用你的私有方法时,你method_missing 将永远不会捕获调用。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-05-10
            • 2011-03-27
            • 2011-02-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-10-14
            相关资源
            最近更新 更多