与其他一些答案相反,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 的parser造成的。由于我们只考虑单个范围,范围规则与问题无关。在解析阶段,解析器决定局部变量在哪些源位置可见,并且这种可见性在执行期间不会改变。
当确定是否在代码中的任何位置定义了局部变量(即defined?
返回 true)时,解析器会检查当前范围以查看之前是否有任何代码分配过它,即使该代码从未运行过(解析器无法知道关于在解析阶段运行或未运行的任何内容)。“之前”的意思:在上一行,或在同一行的左侧。
确定本地是否已定义(即可见)的练习
请注意,以下仅适用于局部变量,不适用于方法。(确定一个方法是否在作用域中定义更复杂,因为它涉及搜索包含的模块和祖先类。)
查看局部变量行为的具体方法是在文本编辑器中打开文件。还假设通过反复按左箭头键,您可以在整个文件中向后移动光标。现在假设您想知道 的某种用法是否message
会提高NameError
. 为此,请将光标放在您正在使用的位置message
,然后按住左箭头,直到您:
- 到达当前作用域的开头(你必须了解 ruby 的作用域规则才能知道什么时候发生)
- 达到分配的代码
message
如果您在到达范围边界之前已经完成了分配,这意味着您的使用message
不会 raise 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
case 时,dest_arr
再次可见,因为解释器看到它是预先定义的(2 行)。
- 另请注意,它
dest_arr
仅在循环开始时隐藏;它的价值永远不会丢失。
这也解释了为什么在while
循环解决问题之前分配本地。分配不需要执行;它只需要出现在源代码中。
拉姆达示例
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'.
通过在's body之前放置一个f2
赋值来解决这个问题。f1
请记住,分配实际上并不需要执行!
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
方法变为“未屏蔽”/“未屏蔽”,以便该方法是可调用的,不带括号。但是赋值后的任何代码都会看到局部变量。
我们可以从中得出一些最佳实践
- 不要将局部变量命名为与同一范围内的方法名称相同
- Don't put the initial assignment of a local variable in the body of a
while
or for
loop, or anything that causes execution to jump around within a scope (calling lambdas or Continuation#call
can do this too). Put the assignment before the loop.