Ruby在while循环中忘记了局部变量?
我正在处理一个基于记录的文本文件:所以我正在寻找一个构成记录开头的起始字符串:没有记录结束标记,所以我使用下一条记录的开头来划分最后的记录。
所以我已经构建了一个简单的程序来实现这一点,但是我看到一些让我感到惊讶的事情:看起来Ruby似乎忘记了局部变量 – 或者我发现了编程错误? [虽然我不认为我有:如果我在循环之前定义变量’message’我没有看到错误]。
这是一个简单的示例,其中包含示例输入数据和注释中的错误消息:
flag=false # message=nil # this is will prevent the issue. while line=gets do if line =~/hello/ then if flag==true then puts "#{message}" end message=StringIO.new(line); puts message flag=true else message << line end end # Input File example: # hello this is a record # this is also part of the same record # hello this is a new record # this is still record 2 # hello this is record 3 etc etc # # Error when running: [nb, first iteration is fine] # # hello # test.rb:5: undefined local variable or method `message' for main:Object (NameError) #
我认为这是因为消息是在循环内定义的。 在循环迭代结束时,“消息”超出了范围。 在循环外定义“消息”会使变量在每次循环迭代结束时停止超出范围。 所以我认为你有正确的答案。
您可以在每次循环迭代开始时输出消息的值,以测试我的建议是否正确。
来自Ruby编程语言:
替代文字http://bks0.books.google.com/books?id=jcUbTcr5XWwC&printsec=frontcover&img=1&zoom=5&sig=ACfU3U1rnYKha_p7vEkpPm1Ow3o9RAM0nQ
块和变量范围
块定义新的变量范围:块内创建的变量仅存在于该块内,并且在块外部未定义。 但是要小心; 方法中的局部变量可用于该方法中的任何块。 因此,如果块为已经在块外部定义的变量赋值,则不会创建新的块局部变量,而是为已存在的变量分配新值。 有时,这正是我们想要的行为:
total = 0 data.each {|x| total += x } # Sum the elements of the data array puts total # Print out that sum
但是,有时我们不想在封闭范围内更改变量,但我们无意中这样做了。 这个问题是Ruby 1.8中块参数的一个特别关注点。 在Ruby 1.8中,如果块参数共享现有变量的名称,则块的调用只是为该现有变量赋值,而不是创建新的块局部变量。 例如,以下代码是有问题的,因为它使用与两个嵌套块的块参数相同的标识符i:
1.upto(10) do |i| # 10 rows 1.upto(10) do |i| # Each has 10 columns print "#{i} " # Print column number end print " ==> Row #{i}\n" # Try to print row number, but get column number end
Ruby 1.9是不同的:块参数总是在它们的块本地,并且块的调用永远不会为现有变量赋值。 如果使用-w标志调用Ruby 1.9,它将警告您块参数是否与现有变量同名。 这有助于您避免编写在1.8和1.9中运行不同的代码。
Ruby 1.9在另一个重要方面也有所不同。 块语法已扩展为允许您声明保证为本地的块局部变量,即使封闭范围中已存在同名的变量也是如此。 为此,请使用分号和逗号分隔的块局部变量列表来跟随块参数列表。 这是一个例子:
x = y = 0 # local variables 1.upto(4) do |x;y| # x and y are local to block # x and y "shadow" the outer variables y = x + 1 # Use y as a scratch variable puts y*y # Prints 4, 9, 16, 25 end [x,y] # => [0,0]: block does not alter these
在此代码中,x是一个块参数:当使用yield调用块时,它获取一个值。 y是块局部变量。 它不会从yield调用中接收任何值,但它的值为nil,直到该块实际为其赋予其他值。 声明这些块局部变量的目的是保证您不会无意中破坏某些现有变量的值。 (例如,如果将块从一个方法剪切并粘贴到另一个方法,则可能会发生这种情况。)如果使用-w选项调用Ruby 1.9,它将警告您块本地变量是否隐藏现有变量。
当然,块可以有多个参数和多个局部变量。 这是一个包含两个参数和三个局部变量的块:
hash.each {|key,value; i,j,k| ... }
与其他一些答案相反, 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
代码之前。
如果事先未定义,则此代码引发错误的原因相同:
# 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 pa # prints nil end
而循环测试案例
这是一个小的测试用例,用于演示上述行为在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() # 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
方法变为“ dest_arr
”/“unhadowed”这样该方法可以用括号括起来调用。 但是赋值后的任何代码都会看到局部变量。
我们可以从这一切中得出一些最佳实践
- 不要将局部变量命名为与同一范围内的方法名称相同
- 不要将局部变量的初始赋值放在
while
或for
循环的主体中,或者导致执行在范围内跳转的任何东西(调用lambdas或Continuation#call
#call也可以这样做)。 在循环之前放置赋值。
为什么你认为这是一个错误? 解释器告诉您,当特定代码段执行时,消息可能未定义。
我不确定为什么你会感到惊讶:在第5行(假设message = nil
行不存在),你可能会使用解释器以前从未听说过的变量。 翻译说“什么是message
?这不是我知道的方法,它不是我知道的变量,它不是关键字……”然后你得到一条错误信息。
这是一个更简单的例子,向您展示我的意思:
while line = gets do if line =~ /./ then puts message # How could this work? message = line end end
这使:
telemachus ~ $ ruby test.rb < huh test.rb:3:in `': undefined local variable or method `message' for main:Object (NameError)
另外,如果你想为message
准备方法,我会把它初始化为message = ''
,这样它就是一个字符串(而不是nil
)。 否则,如果您的第一行与hello 不匹配,您最终会将line
添加到nil
– 这将导致此错误:
telemachus ~ $ ruby test.rb < huh test.rb:4:in `': undefined method `<<' for nil:NilClass (NoMethodError)
你可以这样做:
message='' while line=gets do if line =~/hello/ then # begin a new record p message unless message == '' message = String.new(line) else message << line end end # hello this is a record # this is also part of the same record # hello this is a new record # this is still record 2 # hello this is record 3 etc etc