每当您进入调试会话时,您实际上是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 的本地表传递给 eval。
首先,让我们使用一个非常简单的方法来演示我们所期望的行为。
def foo_boom
foo # => NameError
foo = 1 # => 1
foo # => 1
end
我们可以通过提取现有方法的 YARV 字节码来检查这一点RubyVM::InstructionSequence.disasm(method)
。注意我将忽略跟踪调用以保持指令整洁。
RubyVM::InstructionSequence.disasm(method(:foo_boom))
更少跟踪的输出:
== disasm: #<ISeq:foo_boom@(irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, 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: -1@-1, 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 编译函数调用:
- 推送接收器:
putself
,指的是顶层功能范围
- 推送参数:这里没有
- 调用方法/函数:函数调用(FCALL)到
foo
接下来,我们将在 getfoo
成为全局变量时进行设置:
0008 pop
0011 putobject_OP_INT2FIX_O_1_C_
0012 setlocal_OP__WC__0 2
0016 getlocal_OP__WC__0 2
0020 leave ( 253)
关键要点:当 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:bar_boom@(irb)>=======================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, 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)
我们再次在索引 2 处的 locals 表中看到一个局部变量bar
。我们还有以下 eval 指令:
0004 putself
0005 putstring "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
让我们剖析这里发生的事情:
- Push接收器:再次
putself
,指的是顶级功能范围
- 推送参数:“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
是动态评估的,并且直到运行时才会生成其代码的指令,因此评估发生在已经确定的指令和本地表的上下文中,该表包含bar
您看到的幻象。不幸的是,在这个阶段,Ruby 不知道bar
是在 eval 语句“下方”初始化的。
为了更深入地了解,我建议阅读Ruby Under a Microscope和Ruby Hacking Guide 的评估部分。