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

h9a6wy2h  于 2023-02-08  发布在  Ruby
关注(0)|答案(6)|浏览(129)

我正在处理一个基于记录的文本文件:所以我在寻找一个起始字符串,它构成了一条记录的开始:没有记录结束标记,所以我使用下一个记录的开始来分隔最后一个记录。
所以我已经建立了一个简单的程序来做这个,但是我看到了一些让我吃惊的事情:看起来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)
#
zzzyeukh

zzzyeukh1#

从Ruby编程语言:

(来源:google.com

块和变量范围

块定义新的变量范围:在一个块内创建的变量只存在于该块内,在块外是未定义的。2但是要小心;一个方法中的局部变量对该方法中的任何块都是可用的。因此,如果一个块给一个已经在块外定义的变量赋值,这并不是创建一个新的块局部变量,而是给已经存在的变量赋值。有时候,这正是我们想要的行为:

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是不同的:块参数对于它们的块总是局部的,并且块的调用从不给已有的变量赋值。2如果Ruby 1.9是用-w标志调用的,那么如果块参数和已有的变量同名,它会警告你。3这有助于你避免编写在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| ... }
vbopmzt1

vbopmzt12#

与其他一些答案相反,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的作用域规则,以便知道何时发生这种情况)
1.分配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再次可见,因为解释器看到它在上面被分配了两行。
  • 还要注意,dest_arr只在循环开始时是 * hidden * 的;其价值永远不会丧失(如最终含量为[0, 1]所证明的)。

这也解释了为什么在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循环测试类似,不同之处在于,它不是在赋值代码上方变为未定义,方法变成"unmasked "/" unshaded",这样方法就可以不带括号调用了。但是赋值之后的任何代码都可以看到局部变量。

    • 我们可以从所有这些中获得一些最佳实践**
  • 在同一作用域中,不要将局部变量命名为与方法名称相同的名称
  • 不要将局部变量的初始赋值语句放在whilefor循环体中,或者任何会导致执行在作用域中跳转的语句(调用lambdas或Continuation#call也可以做到这一点)。
bjp0bcyl

bjp0bcyl3#

我认为这是因为消息是在循环内部定义的。在循环迭代结束时,“消息”超出了作用域。在循环外部定义“消息”可以防止变量在每次循环迭代结束时超出作用域。所以我认为你的答案是正确的。
您可以在每次循环迭代开始时输出message的值,以测试我的建议是否正确。

qmelpv7a

qmelpv7a4#

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

ar5n3qh5

ar5n3qh55#

我不知道你有什么好惊讶的在第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)
umuewwlo

umuewwlo6#

你可以简单地这样做:

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

相关问题