Ruby在while循环中忘记了局部变量?

时间:2009-10-31 15:02:12

标签: ruby object undefined

我正在处理一个基于记录的文本文件:所以我正在寻找一个构成记录开头的起始字符串:没有记录结束标记,所以我使用下一条记录的开头划定最后一条记录。

所以我已经构建了一个简单的程序来实现这一点,但是我看到一些让我感到惊讶的事情:看起来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]
# <StringIO:0x2e845ac>
# hello
# test.rb:5: undefined local variable or method `message' for main:Object (NameError)
#

6 个答案:

答案 0 :(得分:29)

来自Ruby编程语言:

alt text 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,则会在块局部变量遮蔽现有变量时发出警告。 / p>

当然,块可以有多个参数和多个局部变量。这是一个包含两个参数和三个局部变量的块:

hash.each {|key,value; i,j,k| ... }

答案 1 :(得分:12)

与其他一些答案相反,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)时,解析器检查当前范围以查看之前是否有任何代码已分配,即使该代码从未run(解析器无法知道在解析阶段运行或未运行的内容)。 “之前”含义:在上面的一条线上,或在同一条线上和左侧。

确定本地是否已定义(即可见)的练习

请注意,以下内容仅适用于局部变量,而不适用于方法。 (确定方法是否在范围中定义更复杂,因为它涉及搜索包含的模块和祖先类。)

查看局部变量行为的具体方法是在文本编辑器中打开文件。还假设通过反复按左箭头键,您可以将光标向后移动整个文件。现在假设您想知道某个message的使用是否会提升NameError。要执行此操作,请将光标放在您正在使用的位置message,然后按住向左箭头,直到您:

  1. 到达当前范围的开头(您必须了解ruby的范围规则才能知道何时发生这种情况)
  2. 到达指定message
  3. 的代码

    如果您在到达范围边界之前已达成作业,则表示您对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]
    • 但我们需要在第二次迭代中密切关注(当n1时):
      • 一开始,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'.
    

    通过在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 < em>方法变为“unmasked”/“unhadowed”,以便该方法可以用括号括起来调用。但是赋值后的任何代码都会看到局部变量。

    我们可以从所有这些

    中获得一些最佳实践
    • 不要将局部变量命名为与同一范围内的方法名称相同
    • 不要将局部变量的初始赋值放在whilefor循环的主体中,或者导致执行在范围内跳转的任何内容(调用lambdas或{{1也可以这样做)。将作业放在循环之前。

答案 2 :(得分:8)

我认为这是因为消息是在循环内定义的。在循环迭代结束时,“消息”超出了范围。在循环外定义“消息”会阻止变量在每次循环迭代结束时超出范围。所以我认为你有正确的答案。

您可以在每次循环迭代开始时输出消息的值,以测试我的建议是否正确。

答案 3 :(得分:2)

为什么你认为这是一个错误?解释器告诉您,当特定代码段执行时,消息可能未定义。

答案 4 :(得分:2)

我不确定为什么你会感到惊讶:在第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 `<main>': undefined local variable or method `message' for main:Object (NameError)

另外,如果你想为message做准备,我会把它初始化为message = '',这样它就是一个字符串(而不是nil)。否则,如果您的第一行 不匹配问候语,那么您最终会将line添加到nil - 这将导致此错误:

telemachus ~ $ ruby test.rb < huh 
test.rb:4:in `<main>': undefined method `<<' for nil:NilClass (NoMethodError)

答案 5 :(得分:0)

你可以这样做:

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