与其他一些答案相反,while 循环实际上并没有创建新的范围。您看到的问题更加微妙。
必备知识:一个简短的范围界定演示
为了帮助显示对比,传递给方法调用的块DO创建一个新的范围,这样块内新分配的局部变量在块退出后消失:
### block example - provided for contrast only ###
[0].each {|e| blockvar = e }
p blockvar # NameError: undefined local variable or method
但while 循环(如您的情况)是不同的,因为循环中定义的变量将持续存在:
arr = [0]
while arr.any?
whilevar = arr.shift
end
p whilevar # prints 0
“问题”总结
在您的案例中出现错误的原因是因为使用message 的行:
puts "#{message}"
出现在任何分配message的代码之前。
如果没有事先定义a,这与此代码引发错误的原因相同:
# Note the single (not double) equal sign.
# At first glance it looks like this should print '1',
# because the 'a' is assigned before (time-wise) the puts.
puts a if a = 1
不是作用域,而是解析可见性
所谓的“问题”——即单个范围内的局部变量可见性——是由于 ruby 的 解析器造成的。由于我们只考虑单个范围,范围规则与问题无关。在解析阶段,解析器决定局部变量在哪些源位置可见,并且此可见性在执行期间不会改变。
当确定是否在代码中的任何位置定义了局部变量(即defined? 返回 true)时,解析器会检查当前范围以查看之前是否有任何代码分配过它,即使该代码从未运行过(解析器无法知道在解析阶段已经运行或未运行的任何内容)。 “之前”的意思:在上一行,或在同一行的左侧。
确定本地是否已定义(即可见)的练习
请注意,以下仅适用于局部变量,不适用于方法。 (确定一个方法是否定义在一个作用域中比较复杂,因为它涉及到搜索包含的模块和祖先类。)
查看局部变量行为的具体方法是在文本编辑器中打开文件。还假设通过反复按左箭头键,您可以在整个文件中向后移动光标。现在假设您想知道message 的某种用法是否会提高NameError。为此,请将光标放在您使用message 的位置,然后按住左箭头直到您:
- 到达当前作用域的开头(您必须了解 ruby 的作用域规则才能知道何时发生这种情况)
- 到达分配
message的代码
如果您在到达范围边界之前就已完成分配,这意味着您对 message 的使用不会引发 NameError。如果你没有完成任何任务,使用会提高NameError。
其他注意事项
如果变量赋值出现在代码中但未运行,则将变量初始化为nil:
# a is not defined before this
if false
# never executed, but makes the binding defined/visible to the else case
a = 1
else
p a # prints nil
end
While循环测试用例
这里有一个小测试用例来演示上述行为在 while 循环中发生时的奇怪性。这里受影响的变量是dest_arr。
arr = [0,1]
while n = arr.shift
p( n: n, dest_arr_defined: (defined? dest_arr) )
if n == 0
dest_arr = [n]
else
dest_arr << n
p( dest_arr: dest_arr )
end
end
哪个输出:
{:n=>0, :dest_arr_defined=>nil}
{:n=>1, :dest_arr_defined=>nil}
{:dest_arr=>[0, 1]}
重点:
- 第一次迭代很直观,
dest_arr 被初始化为[0]。
- 但是在第二次迭代中我们需要密切关注(当
n是1时):
- 一开始,
dest_arr 是未定义的!
- 但是当代码到达
else 的情况下,dest_arr 再次可见,因为解释器看到它是预先定义的(2 行)。
- 还要注意,
dest_arr 仅在循环开始时隐藏;它的价值永远不会丢失。
这也解释了为什么在 while 循环之前分配本地可以解决问题。分配不需要执行;它只需要出现在源代码中。
Lambda 示例
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
# The following fails because the body of f1 tries to access f2 before an assignment for f2 was seen by the parser.
p f1.call() # undefined local variable or method `f2'.
通过在f1 的正文之前放置一个f2 赋值来解决此问题。请记住,实际上并不需要执行分配!
f2 = nil # Could be replaced by: if false; f2 = nil; end
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
p f1.call() # ok
方法屏蔽问题
如果你有一个与方法同名的局部变量,事情会变得非常棘手:
def dest_arr
:whoops
end
arr = [0,1]
while n = arr.shift
p( n: n, dest_arr: dest_arr )
if n == 0
dest_arr = [n]
else
dest_arr << n
p( dest_arr: dest_arr )
end
end
输出:
{:n=>0, :dest_arr=>:whoops}
{:n=>1, :dest_arr=>:whoops}
{:dest_arr=>[0, 1]}
作用域中的局部变量赋值将“屏蔽”/“隐藏”同名的方法调用。 (您仍然可以通过使用显式括号或显式接收器来调用该方法。)所以这类似于之前的 while 循环测试,除了在分配代码上方变为未定义,dest_arr 方法 变为“unmasked”/“unshadowed”,因此该方法可以在没有括号的情况下调用。但是赋值后的任何代码都会看到局部变量。
我们可以从中得出一些最佳实践
- 不要将局部变量命名为与同一范围内的方法名相同
- 不要将局部变量的初始赋值放在
while 或 for 循环的主体中,或者任何导致执行在范围内跳转的任何内容(调用 lambdas 或 Continuation#call 也可以这样做)。将赋值放在循环之前。