请参阅此简单的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
,它将遮蔽方法bar
,而调试器实际上不再可以调用bar
方法。目前不知道的是字符串'local string'
将分配给bar
。调试器返回bar
的nil
。
让我们继续使用Ruby文件中的原始代码,然后查看其输出:
(byebug) continue
method
"string from method"
local-variable
"local string"
在运行时,#7
Ruby仍然知道bar
确实是一种方法,并且仍然能够将其称为#8
行中。然后,l Ine #10
实际上定义了以相同名称阴影的局部变量,因此Ruby像LINE #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说明,本地表格,该表存储了该方法的参数和所有本地变量的信息,以及该方法中的所有局部变量,以及在方法中包含有关RESISE的信息如果存在。
此问题的根本原因是,由于您在运行时使用eval
动态编译代码,因此Ruby通过本地表,其中包括一个不设置变量ENRY,也可以进行评估。
开始,让我们使用一种非常简单的方法来证明我们期望的行为。
def foo_boom
foo # => NameError
foo = 1 # => 1
foo # => 1
end
我们可以通过使用RubyVM::InstructionSequence.disasm(method)
提取现有方法的YARV字节代码来对其进行检查。请注意,我将忽略跟踪呼叫以保持说明整洁。
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
,并将其存储在我们的index [2]的本地表中。如果我们有其他本地变量和论点,它们也会出现在此表中。
接下来,我们将在分配foo
之前生成的说明:
0004 putself
0005 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0008 pop
让我们剖析这里发生的情况。Ruby Commiles函数根据以下模式要求YARV:
- 推送接收器:
putself
,指函数的顶级范围 - 推荐参数:这里没有
- 调用方法/函数:函数调用(fcall)to
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拥有整个源代码时,它知道何时定义当地人,并且将过早调用对本地变量作为fcalls,就像您期望的那样。
现在让我们看一个使用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的本地表中看到了本地变量bar
0004 putself
0005 putstring "bar"
0007 opt_send_without_block <callinfo!mid:eval, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0010 pop
让我们解剖此处会发生什么:
- 推送接收器:再次
putself
,指函数的顶级范围 - 推荐参数:" bar"
- 调用方法/函数:函数调用(fcall)to
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
进行了动态评估,并且直到运行时才能生成其代码的说明,因此评估发生在已经确定的指令和本地表的上下文中,该指令和本地表中保留了您看到的Phantom bar
。不幸的是,在此阶段,Ruby不知道bar
是在" eval语句"中初始化的。
要进行更深入的潜水,我建议您在显微镜下阅读Ruby和Ruby Hacking指南的评估部分。