加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

为什么Ruby调试器在运行时返回与代码不同的值?

发布时间:2020-12-16 20:08:00 所属栏目:百科 来源:网络整理
导读:看到这个简单的 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' endendFoo.new.run 运行此类时,在调试器的控制台中
看到这个简单的 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的函数调用:

推送接收器:自己,参考顶级功能范围
>推论据:无
调用方法/函数:函数调用(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.

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读