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

mon*_*nny 15 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)
#
Run Code Online (Sandbox Code Playgroud)

JRL*_*JRL 29

从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
Run Code Online (Sandbox Code Playgroud)

但是,有时我们不想在封闭范围内更改变量,但我们无意中这样做了.这个问题是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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

在此代码中,x是一个块参数:当使用yield调用块时,它获取一个值.y是块局部变量.它不会从yield调用中接收任何值,但它的值为nil,直到块实际为其分配其他值.声明这些块局部变量的目的是保证您不会无意中破坏某些现有变量的值.(例如,如果将块从一个方法剪切并粘贴到另一个方法,则可能会发生这种情况.)如果使用-w选项调用Ruby 1.9,它将警告您块本地变量是否隐藏现有变量.

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

hash.each {|key,value; i,j,k| ... }
Run Code Online (Sandbox Code Playgroud)


Kel*_*vin 12

与其他一些答案相反,while循环实际上并不创建新范围.你看到的问题更加微妙.

为了帮助显示对比度,传递给方法调用的块DO创建一个新范围,以便块退出后块内新分配的局部变量消失:

### block example - provided for contrast only ###
[0].each {|e| blockvar = e }
p blockvar  # NameError: undefined local variable or method
Run Code Online (Sandbox Code Playgroud)

但是while循环(就像你的情况一样)是不同的:

arr = [0]
while arr.any?
  whilevar = arr.shift
end
p whilevar  # prints 0
Run Code Online (Sandbox Code Playgroud)

你在案件中得到错误的原因是因为使用的行message:

puts "#{message}"
Run Code Online (Sandbox Code Playgroud)

出现在任何指定的 代码之前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
Run Code Online (Sandbox Code Playgroud)

没有范围,但解析可见性

所谓的"问题" - 即单个范围内的局部变量可见性 - 是由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 
Run Code Online (Sandbox Code Playgroud)

而循环测试案例

这是一个小的测试用例,用于演示上述行为在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
Run Code Online (Sandbox Code Playgroud)

哪个输出:

{:n=>0, :dest_arr_defined=>nil}
{:n=>1, :dest_arr_defined=>nil}
{:dest_arr=>[0, 1]}
Run Code Online (Sandbox Code Playgroud)

重点:

  • 第一次迭代是直观的,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'.
Run Code Online (Sandbox Code Playgroud)

通过在身体f2前面进行任务来解决这个问题f1.请记住,分配实际上并不需要执行!

f2 = nil  # Could be replaced by: if false; f2 = nil; end
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
p f1.call()  # ok
Run Code Online (Sandbox Code Playgroud)

方法掩盖陷阱

如果你有一个与方法同名的局部变量,事情变得非常多毛:

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
Run Code Online (Sandbox Code Playgroud)

输出:

{:n=>0, :dest_arr=>:whoops}
{:n=>1, :dest_arr=>:whoops}
{:dest_arr=>[0, 1]}
Run Code Online (Sandbox Code Playgroud)

范围中的局部变量赋值将"掩盖"/"影子"具有相同名称的方法调用.(您仍然可以通过使用显式括号或显式接收器来调用该方法.)因此,这类似于之前的while循环测试,除了不是在赋值代码之上变为未定义,该dest_arr 方法变为"unmasked"/"unhadowed",以便该方法可以用括号括起来调用.但是赋值后的任何代码都会看到局部变量.

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

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

  • 非常好.这解释了为什么这个循环永远不会结束:`直到定义?(foo)do foo = 1 end`但是这个结果:`begin foo = 1 end,直到定义?(foo)` (3认同)

ric*_*chj 8

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

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