14

查看这个简单的 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- 它已经知道bar在线定义的局部变量#10将隐藏该方法bar,并且调试器实际上无法再调用该bar方法。目前不知道的是该字符串'local string'将被分配给bar. 调试器返回nil.bar

让我们继续 Ruby 文件中的原始代码并查看其输出:

    (byebug) continue
    method
    "string from method"
    local-variable
    "local string"

在运行时,#7Ruby 仍然知道这bar确实是一个方法,并且它仍然能够在 line 中调用它#8。然后 line#10实际上定义了隐藏具有相同名称的方法的局部变量,并且 tTherefore Ruby 像预期的那样返回 line#12#13

问题:为什么调试器返回的值与原始代码不同?它似乎能够展望未来。这被认为是功能还是错误?这种行为是否记录在案?

4

1 回答 1

6

每当您进入调试会话时,您实际上是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 MicroscopeRuby Hacking Guide 的评估部分。

于 2017-03-14T05:03:58.820 回答