看到这个简单的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
会影响方法{{1并且调试器实际上不再能够调用bar
方法。此时不知道的是字符串bar
将分配给'local string'
。调试器返回bar
的{{1}}。
让我们继续使用Ruby文件中的原始代码并查看其输出:
nil
在第bar
行中的运行时,Ruby仍然知道 (byebug) continue
method
"string from method"
local-variable
"local string"
确实是一种方法,它仍然可以在行#7
中调用它。然后l ine bar
实际定义了使用相同名称隐藏方法的局部变量,因此Ruby返回行#8
和#10
中的预期。
问题:为什么调试器返回的值与原始代码不同?看起来它能够展望未来。这被视为功能还是错误?这种行为是否记录在案?
答案 0 :(得分:3)
每当您进入调试会话时,您都会有效地对代码中该位置的绑定执行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))
减少跟踪的输出:
== 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
,指的是顶级功能范围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,就像你期望的那样。
现在让我们来看看行为不端的行为&#34;使用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
让我们剖析一下这里发生的事情:
putself
,指的是顶级功能范围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
是动态评估的,并且其代码的指令不会生成直到运行时,因此评估发生在已经确定的指令和本地表的上下文中,该表保存了幻像{{1}你看到了。不幸的是,在这个阶段,Ruby没有意识到bar
被初始化&#34;低于&#34;评估声明。
如果想进行更深入的潜水,我建议您阅读Ruby Under a Microscope和评估的Ruby Hacking Guide's部分。