require 'byebug' class Foo def run byebug puts defined?(bar) puts bar.inspect bar = 'local string' puts defined?(bar) puts bar.inspect end def bar 'string from method' end end Foo.new.run
运行此类时,在调试器的控制台中可以看到以下行为:
$ruby byebug.rb [2,11] in /../test.rb 2: 3: class Foo 4: def run 5: byebug 6: => 7: puts defined?(bar) 8: puts bar.inspect 9: 10: bar = 'local string' 11:
在断点处,调试器返回以下值:
(byebug) defined?(bar) "local-variable" (byebug) bar.inspect "nil"
请注意,尽管调试器的断点位于第5行 – 它已经知道将在第10行中定义一个局部变量条,该变量条将影响方法栏,调试器实际上不再可以调用bar方法.什么是不知道在这一点是字符串’本地字符串’将分配给吧.调试器返回nil为bar.
(byebug) continue method "string from method" local-variable "local string"
在第7行的运行时,Ruby仍然知道该栏确实是一种方法,它仍然可以在第8行中调用.然后,l ine#10实际上定义了使用相同名称阴影方法的局部变量,因此Ruby返回像第12行和第13行那样预期.
解决方法
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指令,本地表存储有关方法的参数和方法中的所有局部变量的信息,以及捕获表,其中包含有关该方法中的救援信息(如果存在)的捕获表.
这个问题的根本原因是,由于您在运行时使用eval来动态编译代码,所以Ruby也将本地表(其中包含一个未设置的变量enry)传递给eval.
首先,我们使用一个非常简单的方法来演示我们期望的行为.
def foo_boom foo # => NameError foo = 1 # => 1 foo # => 1 end
我们可以通过使用RubyVM :: InstructionSequence.disasm(method)提取现有方法的YARV字节码来检查这一点.注意我将忽略跟踪调用以保持指示整洁.
输出为RubyVM :: InstructionSequence.disasm(method(:foo_boom))less trace:
== 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,kwrest: -1]) [ 2] foo
我们可以在这里看到,YARV已经确定我们有本地变量foo,并将其存储在我们的本地表中的index [2].如果我们有其他局部变量和参数,它们也会出现在此表中.
接下来,当我们尝试在调用foo之前调用foo时,会产生指令:
0004 putself 0005 opt_send_without_block <callinfo!mid:foo,<callcache> 0008 pop
让我们来剖析这里发生了什么. Ruby根据以下模式编译YARV的函数调用:
推送接收器:自己,参考顶级功能范围
>推论据:无
调用方法/函数:函数调用(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)
关键外包:当YARV拥有手头的全部源代码时,它知道当地人被定义,并将过早调用局部变量视为FCALL,就像您所期望的那样.
现在我们来看一下使用eval的“不正常行为”版本
def bar_boom eval 'bar' # => nil,but we'd expect an errror bar = 1 # => 1 bar end
RubyVM :: InstructionSequence.disasm的输出(方法(:bar_boom))less trace:
== disasm: #<ISeq:bar_boom@(irb)>======================================= local table (size: 2,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)
再次,我们在index2的locals表中看到一个局部变量bar.我们还有以下eval命令:
0004 putself 0005 putstring "bar" 0007 opt_send_without_block <callinfo!mid:eval,<callcache> 0010 pop
我们来剖析这里发生了什么:
推送接收机:再一次提及顶级的功能范围
>推参数:“bar”
调用方法/函数:函数调用(FCALL)到eval
之后,我们有标准的作业来禁止我们的期望.
0013 putobject_OP_INT2FIX_O_1_C_ 0014 setlocal_OP__WC__0 2 0018 getlocal_OP__WC__0 2 0022 leave ( 264)
如果我们没有在这里进行评估,Ruby就会把这个调用作为一个函数调用来处理,这个函数调用将像以前的例子那样被破坏.然而,由于eval是动态评估的,并且直到运行时才会生成其代码的指令,所以评估将在已经确定的指令和本地表的上下文中进行,该表保存了您看到的幻像棒.不幸的是,在这个阶段,Ruby并不知道该栏被初始化为eval语句.
为了更深入的潜水,我建议您阅读Ruby Under a Microscope和评估版Ruby Hacking Guide’s.