Ответ 1
Всякий раз, когда вы заходите в сеанс отладки, вы эффективно выполняете eval
против привязки в этом месте кода. Здесь приведен более простой бит кода, который воссоздает поведение, приводящее вас в бешенство:
def make_head_explode
puts "== Proof bar isn't defined"
puts defined?(bar) # => nil
puts "== But WTF?! It shows up in eval"
eval(<<~RUBY)
puts defined?(bar) # => 'local-variable'
puts bar.inspect # => nil
RUBY
bar = 1
puts "\n== Proof bar is now defined"
puts defined?(bar) # => 'local-variable'
puts bar.inspect # => 1
end
Когда метод make_head_explode
подается в интерпретатор, он скомпилирован в инструкции YARV, локальную таблицу, в которой хранятся сведения о аргументах метода и всех локальных переменных в методе, а таблица catch, которая содержит информацию о спасениях внутри метод, если он есть.
Основной причиной этой проблемы является то, что, поскольку вы динамически компилируете код во время выполнения с помощью eval
, Ruby передает локальную таблицу, которая включает в себя неустановленную переменную enry, для оценки.
Чтобы начать, используйте использование очень простого метода, который демонстрирует поведение, которое мы ожидаем.
def foo_boom
foo # => NameError
foo = 1 # => 1
foo # => 1
end
Мы можем проверить это, извлекая байт-код YARV для существующего метода с помощью RubyVM::InstructionSequence.disasm(method)
. Примечание. Я собираюсь игнорировать вызовы трассировки, чтобы сохранить инструкции.
Выход для RubyVM::InstructionSequence.disasm(method(:foo_boom))
меньше трассы:
== disasm: #<ISeq:[email protected](irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: [email protected], kwrest: -1])
[ 2] foo
0004 putself
0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave ( 253)
Теперь пройдитесь по трассе.
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: [email protected], kwrest: -1])
[ 2] foo
Здесь мы видим, что YARV идентифицировал локальную переменную foo
и сохранил ее в нашей локальной таблице в индексе [2]. Если бы у нас были другие локальные переменные и аргументы, они также появлялись в этой таблице.
Затем у нас есть команды, созданные при попытке вызвать foo
до его назначения:
0004 putself
0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0008 pop
Позвольте рассказать, что здесь происходит. Ruby компилирует вызовы функций для YARV в соответствии со следующим шаблоном:
- Push-приемник:
putself
, ссылаясь на область действия верхнего уровня - Push аргументы: нет здесь
- Вызвать метод/функцию: вызов функции (FCALL) на
foo
Далее у нас есть инструкции по настройке при получении foo
, когда она станет глобальной переменной:
0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave ( 253)
Key takeaway: когда YARV имеет весь исходный код под рукой, он знает, когда локали определены и рассматривает преждевременные вызовы локальных переменных как FCALL, как и ожидалось.
Теперь давайте посмотрим на версию "плохого поведения", которая использует eval
def bar_boom
eval 'bar' # => nil, but we'd expect an errror
bar = 1 # => 1
bar
end
Вывод для RubyVM::InstructionSequence.disasm(method(:bar_boom))
меньше трассы:
== disasm: #<ISeq:[email protected](irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: [email protected], kwrest: -1])
[ 2] bar
0004 putself
0005 putstring "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
0013 putobject_OP_INT2FIX_O_1_C_
0014 setlocal_OP__WC__0 2
0018 getlocal_OP__WC__0 2
0022 leave ( 264)
Снова мы видим локальную переменную bar
в таблице locals в индексе 2. У нас также есть следующие инструкции для eval:
0004 putself
0005 putstring "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
Позвольте рассказать, что здесь происходит:
- Push-приемник: снова
putself
, ссылаясь на область действия верхнего уровня - Push аргументы: "bar"
- Вызвать метод/функцию: вызов функции (FCALL) на
eval
После этого у нас есть стандартное назначение bar
, которое мы ожидаем.
0013 putobject_OP_INT2FIX_O_1_C_
0014 setlocal_OP__WC__0 2
0018 getlocal_OP__WC__0 2
0022 leave ( 264)
Если бы здесь не было eval
, Ruby знал бы, что вызов вызова bar
будет вызван вызовом функции, который бы взорвался, как в предыдущем примере. Однако, поскольку eval
динамически оценивается, и инструкции для его кода не будут генерироваться до выполнения, оценка выполняется в контексте уже определенных инструкций и локальной таблицы, в которой содержится phantom bar
, который вы видите, К сожалению, на этом этапе Ruby не знает, что bar
был инициализирован "ниже" в выражении eval.
Для более глубокого погружения я бы рекомендовал прочитать Ruby Under the Microscope и раздел "Руководство Ruby Hacking Guide" "Оценка".