被调用者获取调用者绑定


在一个方法(被调用者)中如果想要获取调用者的绑定,最简单有效的方法自然是设置一个参数让调用者将其绑定传递过来。

def foo(binding)
  eval("local_variables", binding)
end

bar = 1

p foo(binding) # => [:bar]

然而这种途径会使得每次调用方法都多出一个额外的参数,有时可能会使方法调用显得不太美观。这时,我们可以采用传递块的方式来实现相同的功能——让调用者在调用被调用者时附带一个空的块,并让被调用者将其转换为一个 Proc 对象,然后通过 Proc#binding 获取到调用者的绑定。当然,如果被调用者本身就需要一个块,自然就不用专门在调用时附带一个 dummy 的块了。

def foo(&block)
  eval("local_variables", block.binding)
end

bar = 1

p foo {} # => [:bar]

可是这种方法仍然修改了调用方法的形式。难道在调用者不传递任何信息的情况下被调用者就无法获取调用者的绑定了吗?在 Ruby 语言层,似乎并没有现成的接口提供这样的功能,所以我们只能另辟蹊径。自行创建一个类似于调用栈的“绑定栈”来跟踪不同调用者的绑定就是一个可行的办法,虽然它带来了一定的开销。

class Binding
  @@call_binding_stack = [ nil, TOPLEVEL_BINDING ]
  class << self
    def caller_binding
      @@call_binding_stack[-2]
    end
    def push_caller_binding(binding)
      @@call_binding_stack.push(binding)
    end
    def pop_caller_binding
      @@call_binding_stack.pop
    end
  end
end

set_trace_func lambda { |event, file, line, id, binding, klass|
  return if klass == Binding
  case event
  when 'call'
    Binding.push_caller_binding(binding)
  when 'return'
    Binding.pop_caller_binding
  end
}

def foo
  eval("local_variables", Binding.caller_binding)
end

bar = 0

p foo # => [:bar]

Kernel#set_trace_func 可以让我们安装一个钩子,让解释器在所有方法被调用和返回时都回调我们的钩子过程,也就是这里的 lambda 闭包。方法调用时,我们就将当前的绑定压入绑定栈;方法返回时,我们就出栈。绑定栈初始时有两个元素,一个是 nil,这是因为顶层没有调用者;一个是 TOPLEVEL_BINDING,这是因为在顶层调用的方法,调用者自然是顶层本身。栈的顶部永远是当前(被调用者)的绑定,所以为了获取到调用者的绑定,我们需要返回顶部下面的第一个元素。

Leave a comment