【问题标题】:Ruby forgets local variables during a while loop?Ruby在while循环中忘记了局部变量?
【发布时间】:2009-10-31 15:02:11
【问题描述】:

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

所以我已经构建了一个简单的程序来执行此操作,但我看到了一些让我吃惊的东西:看起来 Ruby 忘记了局部变量的存在 - 或者我发现了一个编程错误? [虽然我不认为我有:如果我在循环之前定义变量“消息”,我看不到错误]。

这是一个简化的示例,其中包含 cmets 中的示例输入数据和错误消息:

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)
#

【问题讨论】:

  • 这是一个很好的问题。我已经编写 ruby​​ 代码至少十年了,但我仍然无法立即看到根本原因。看我的回答。
  • 我已经阅读了你的答案——但我还需要再阅读几遍才能让它深入我厚厚的脑袋。谢谢你。

标签: ruby object undefined


【解决方案1】:

来自 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,它会在块局部变量遮蔽现有变量时发出警告。

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

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

【讨论】:

    【解决方案2】:

    与其他一些答案相反,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 的位置,然后按住左箭头直到您:

    1. 到达当前作用域的开头(您必须了解 ruby​​ 的作用域规则才能知道何时发生这种情况)
    2. 到达分配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]
    • 但是在第二次迭代中我们需要密切关注(当n1时):
      • 一开始,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”,因此该方法可以在没有括号的情况下调用。但是赋值后的任何代码都会看到局部变量。

    我们可以从中得出一些最佳实践

    • 不要将局部变量命名为与同一范围内的方法名相同
    • 不要将局部变量的初始赋值放在 whilefor 循环的主体中,或者任何导致执行在范围内跳转的任何内容(调用 lambdas 或 Continuation#call 也可以这样做)。将赋值放在循环之前。

    【讨论】:

    • +1 关于 ruby​​ 的 while 以及它如何限定看似局部变量的范围的有趣见解。您是否找到任何关于此行为的“官方”文档,说明为什么这是特定于 while 的?
    • @Matt 我进行了编辑以澄清问题不是与范围界定有关。我们只考虑 一个 范围。循环是特殊的,因为它们不符合解析器从左到右、从上到下读取内容的方式,即循环导致执行在源代码中向左或向上跳转。范围内的局部变量可见性在解析阶段是固定的。我不知道官方文档,但this answer 可能会有所帮助。
    • 非常好。这就解释了为什么这个循环永远不会结束:until defined?(foo) do foo=1 end 但这个循环:begin foo=1 end until defined?(foo)
    【解决方案3】:

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

    你可以在每次循环迭代开始时输出message的值来测试我的建议是否正确。

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 恕我直言:我认为这不对。解释器告诉消息此时尚未定义。第一次迭代(放置消息)有效。这必须是由于块中的范围规则。
      【解决方案5】:

      我不知道你为什么感到惊讶:在第 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)。否则,如果您的第一行 匹配 hello,您最终会尝试将 line 添加到 nil - 这将导致您出现此错误:

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

      【讨论】:

      • 澄清我为什么感到惊讶:声明“message=StringIO.new(line);”在我收到“未定义”消息之前已运行一次:(程序确实输出对消息的第一个引用,而不是后续迭代)。我可能应该在我的输出中指出这一点。 [现在将修改]
      【解决方案6】:

      你可以这样做:

      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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-03-30
        • 2018-07-02
        • 2015-12-10
        • 1970-01-01
        相关资源
        最近更新 更多