Как я могу получить привязку метода method_missing?

Я пытаюсь найти способ получить привязку от вызывающего в method_missing в Ruby (1.8), но я не могу найти способ сделать это.

Надеюсь, следующий код объясняет, что я хотел бы сделать:

class A
  def some_method
    x = 123
    nonexistent_method
  end

  def method_missing(method, *args, &block)
    b = caller_binding # <---- Is this possible?
    eval "puts x", b
  end
end

A.new.some_method
# expected output:
#   123

Итак... есть способ получить привязку звонящего, или это просто невозможно в Ruby (1.8)?

Ответ 1

Здесь (несколько хрупкий) взлом:

# caller_binding.rb
TRACE_STACK = []
VERSION_OFFSET = { "1.8.6" => -3, "1.9.1" => -2 }[RUBY_VERSION]
def caller_binding(skip=1)
  TRACE_STACK[ VERSION_OFFSET - skip ][:binding]
end
set_trace_func(lambda do |event, file, line, id, binding, classname|
  item = {:event=>event,:file=>file,:line=>line,:id=>id,:binding=>binding,:classname=>classname}
  #p item
  case(event)
  when 'line'
    TRACE_STACK.push(item) if TRACE_STACK.empty?
  when /\b(?:(?:c-)?call|class)\b/
    TRACE_STACK.push(item)
  when /\b(?:(?:c-)?return|end|raise)\b/
    TRACE_STACK.pop
  end
end)

Это работает с вашим примером, но я не тестировал его с большим количеством

require 'caller_binding'
class A
  def some_method
    x = 123
    nonexistent_method
  end
  def method_missing( method, *args, &block )
    b = caller_binding
    eval "puts x", b
  end
end

x = 456
A.new.some_method #=> prints 123
A.new.nonexistent_method #=> prints 456

Конечно, это не сработает, если привязка не определяет переменную, которую вы пытаетесь оценить, но это общая проблема с привязками. Если переменная не определена, она не знает, что это такое.

require 'caller_binding'
def show_x(b)
  begin
    eval <<-SCRIPT, b
      puts "x = \#{x}"
    SCRIPT
  rescue => e
    puts e
  end
end

def y
  show_x(caller_binding)
end

def ex1
  y #=> prints "undefined local variable or method `x' for main:Object"
  show_x(binding) #=> prints "undefined local variable or method `x' for main:Object"
end

def ex2
  x = 123
  y #+> prints "x = 123"
  show_x(binding) #+> prints "x = 123"
end

ex1
ex2

Чтобы обойти это, вам нужно выполнить некоторую обработку ошибок в оцениваемой строке:

require 'caller_binding'
def show_x(b)
  begin
    eval <<-SCRIPT, b
      if defined? x
        puts "x = \#{x}"
      else
        puts "x not defined"
      end
    SCRIPT
  rescue => e
    puts e
  end
end

def y
  show_x(caller_binding)
end

def ex1
  y #=> prints "x not defined"
  show_x(binding) #=> prints "x not defined"
end

def ex2
  x = 123
  y #+> prints "x = 123"
  show_x(binding) #+> prints "x = 123"
end

ex1
ex2

Ответ 2

Если метод вызывается с помощью блока, вы можете получить привязку блока (которая закрывается по привязке звонящего), выполнив block.binding. Это не работает без блока.

Вы не можете получить привязку звонящего напрямую (ну, если вы не передадите ее явно, конечно).

Изменить: я должен добавить, что когда-то использовался метод Binding.of_caller, но это больше не работает ни с одной из последних рубиновых версий (где последние включают 1.8.6)

Ответ 3

Это может быть немного грязнее, чем вы хотели, но здесь одним способом я смог это сделать.

#x = 1 # can uncomment out this and comment the other if you like

A = Class.new do
  x = 1
  define_method :some_method do
    x = 123
    nonexistent_method
  end

  define_method :method_missing do |method, *args|
    puts x
  end
end

A.new.some_method

Замена определений класса и метода вызовами Class.new и define_method составляет только половину задания. К сожалению, уродливая часть заключается в том, что она работает только в том случае, если вы уже заранее определили x, так что вы на самом деле не захватываете привязку звонящего (вместо этого вызывающая изменяет переменную в другой области).

Это может быть эквивалентно простому определению всех ваших переменных как глобальных, но это может сработать для вас в зависимости от вашей ситуации. И, возможно, с этим вы сможете найти последний кусочек головоломки с этим изменением в руке (если это не сработает для вас).

РЕДАКТИРОВАТЬ: Вы можете получить привязку любого из методов следующим образом, но даже с этим я не могу eval успешно (обязательно поставьте это вверху), Это заполнит @@binding привязками для some_method и method_missing (в этом порядке), поэтому, возможно, это может помочь как-то.

@@binding = []

class Class
  alias real_def define_method
  def define_method(method_name, &block)
    real_def method_name, &block
    @@binding << block.binding
  end
end