为什么Ruby调试器在运行时返回与代码不同的值?
看到这个简单的
Ruby类:
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. 我们继续使用Ruby文件中的原始代码并查看其输出: (byebug) continue method "string from method" local-variable "local string" 在第7行的运行时,Ruby仍然知道该栏确实是一种方法,它仍然可以在第8行中调用.然后,l ine#10实际上定义了使用相同名称阴影方法的局部变量,因此Ruby返回像第12行和第13行那样预期. 问题:为什么调试器返回与原始代码不同的值?看起来好像能够展望未来.这被认为是功能还是错误?这个行为是否记录在案? 解决方法
每当你进入一个调试会话时,你就可以在代码中的那个位置上有效的执行一个针对绑定的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指令,本地表存储有关方法的参数和方法中的所有局部变量的信息,以及捕获表,其中包含有关该方法中的救援信息(如果存在)的捕获表. 这个问题的根本原因是,由于您在运行时使用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的函数调用: 推送接收器:自己,参考顶级功能范围 接下来,我们有一个设置成为一个全局变量的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 我们来剖析这里发生了什么: 推送接收机:再一次提及顶级的功能范围 之后,我们有标准的作业来禁止我们的期望. 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. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |