了解使用method_added动态覆盖实例方法的Ruby元编程

Sen*_*jai 5 ruby singleton metaprogramming ruby-on-rails mixins

我有以下来自Ruby 1.9编程的代码(略有改编),我只是想确保自己的想法正确无误

module Trace
  def self.included(culprit)
    #Inject existing methods with tracing code:
    culprit.instance_methods(false).each do |func|
      inject(culprit, func)
    end

    #Override the singletons method_added to ensure all future methods are injected.
    def culprit.method_added(meth)
      unless @trace_calls_internal
        @trace_calls_internal = true
        Trace.inject(self, meth) #This will call method_added itself, the condition prevents infinite recursion.
        @trace_calls_internal = false
      end
    end
  end

  def self.inject(target, meth)
    target.instance_eval do
      #Get the method
      method_object = instance_method(meth)
      #Rewrite dat sheet
      define_method(meth) do |*args, &block|
        puts "==> Called #{meth} with #{args.inspect}"
        #the bind to self will put the context back to the class.
        result = method_object.bind(self).call(*args, &block)
        puts "<== #{meth} returned #{result.inspect}"
        result
      end
    end
  end
end

class Example
  def one(arg)
    puts "One called with #{arg}"
  end
end
#No tracing for the above.
ex1 = Example.new
ex1.one("Sup") #Not affected by Trace::inject

class Example #extend the class to include tracing.
  include Trace #calls Trace::inject on existing methods via Trace::included
  def two(a1, a2) #triggers Example::method_added(two) and by extension Trace::inject
    a1 + a2
  end
end

ex1.one("Sup again") #Affected by Trace::inject
puts ex1.two(5, 4) #Affected by Trace::inject
Run Code Online (Sandbox Code Playgroud)

我仍在努力解决这个问题。我希望有人能确认我的思维过程,以确保我了解这里发生的事情。这些注释是我自己添加的。我真的认为我对方法绑定,单例类和元编程的理解最多不过是新手。

首先,任何包含它的类都将调用Trace :: included。此方法有两件事,获取该类中现有功能的列表(如果有),并使用inject覆盖其方法。然后,它修改包含该模块的类的单例类,并覆盖默认的method_added方法,以确保每次添加一个方法超过附加的include注入都会影响它。此方法使用一个变量来防止无限递归,因为对inject的调用将根据其性质调用method_added。

Trace ::的工作方式如下:使用instance_eval将self设置为类定义中存在的上下文。因此,将scope(?)修改为该类定义中的范围。

然后,将method_object设置为instance_method(meth),这将获得要添加的原始方法。由于instance_method没有显式的接收者,它将默认为self,它与类定义中的上下文相同吗?

然后,我们使用define_method定义一个具有相同名称的方法。因为我们在instance_eval的上下文中,所以这等效于定义实例方法,并将覆盖现有方法。我们的方法接受任意数量的参数和一个块(如果存在)。

我们添加一些光晕以放置“跟踪”,然后还调用存储在method_object中的原始方法,因为原始方法已被覆盖。此方法是未绑定的,因此我们必须使用bind(self)将其绑定到当前上下文,以便它具有与最初相同的上下文?然后,我们使用call并传递参数和块,存储其返回值,并在打印后返回其返回值。


我真的希望我能对此进行充分描述。我的描述准确吗?对于粗体内容和以下内容,我尤其不确定:

method_object.bind(self).call(*args, &block)
Run Code Online (Sandbox Code Playgroud)

fro*_*ion 2

Trace::的工作原理如下:使用instance_eval将self设置为类定义中存在的上下文。因此,范围(?)被修改为它在该类定义中的样子。

使用实例 eval 可以评估自绑定到对象的块,在本例中,该对象将是包含该模块的类。(即罪魁祸首)。为了清楚起见,以下之间存在区别:

o = Object.new
o.instance_eval do
  puts self
end
Run Code Online (Sandbox Code Playgroud)

class Foo < Object end
Foo.instance_eval do  puts self end
Run Code Online (Sandbox Code Playgroud)

答案:所以是的,你的这个假设是正确的!


然后我们将method_object设置为instance_method(meth),这将获取要添加的原始方法。由于instance_method没有显式接收器,因此它将默认为self,这与类定义中的上下文相同?

是的,你的假设是正确的。请注意,询问时:

culprit.instance_methods(false) => [:someselector, :someotherselector]
Run Code Online (Sandbox Code Playgroud)

在这种情况下调用实例方法确实与调用 self.instance_method 相同。


这个方法是未绑定的,所以我们必须使用bind(self)将它绑定到当前上下文,以便它具有与原来相同的上下文?

是的。当您以跟踪模块中定义的方式获取方法时,您将获得一个未绑定的方法对象,可以按照描述再次绑定该对象。


如果您想深入了解 Ruby 的元编程,我推荐以下书籍:http://pragprog.com/book/ppmetr/metaprogramming-ruby它解释了 Ruby 对象系统、mixins、块以及您能想象到的任何内容背后的所有细节。 。