17

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

所以我构建了一个简单的程序来执行此操作,但我看到了一些让我吃惊的东西:看起来 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)
#
4

6 回答 6

30

来自 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| ... }
于 2009-10-31T15:10:20.920 回答
16

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

  1. 到达当前作用域的开头(你必须了解 ruby​​ 的作用域规则才能知道什么时候发生)
  2. 达到分配的代码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].
  • 但我们需要密切关注第二次迭代(何时n1):
    • 一开始,dest_arr是未定义的!
    • 但是当代码到达elsecase 时,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.
于 2014-01-22T23:52:36.833 回答
7

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

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

于 2009-10-31T15:07:44.920 回答
2

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

于 2009-10-31T15:09:52.060 回答
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)。否则,如果您的第一行与 hello匹配,您最终会尝试添加linenil- 这会给您带来以下错误:

telemachus ~ $ ruby test.rb < huh 
test.rb:4:in `<main>': undefined method `<<' for nil:NilClass (NoMethodError)
于 2009-10-31T15:44:01.107 回答
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
于 2011-01-19T22:41:11.367 回答