17

レコードベースのテキスト ファイルを処理しています。つまり、レコードの開始を構成する開始文字列を探しています。レコードの終了マーカーがないため、次のレコードの開始を使用して区切ります。最後の記録。

これを行うための簡単なプログラムを作成しましたが、驚いたことに、Ruby がローカル変数の存在を忘れているように見えます。それとも、プログラミング エラーを見つけたのでしょうか? [持っているとは思いませんが、ループの前に変数「メッセージ」を定義すると、エラーは表示されません]。

入力データの例とコメント内のエラーメッセージを含む簡単な例を次に示します。

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 では、ブロック パラメーターが既存の変数の名前を共有している場合、ブロックの呼び出しは、新しいブロック ローカル変数を作成するのではなく、その既存の変数に値を割り当てるだけです。たとえば、次のコードは、入れ子になった 2 つのブロックのブロック パラメーターとして同じ識別子 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 を呼び出すと、ブロック ローカル変数が既存の変数をシャドウする場合に警告が表示されます。

もちろん、ブロックは複数のパラメーターと複数のローカル変数を持つことができます。以下は、2 つのパラメーターと 3 つのローカル変数を持つブロックです。

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​​ のパーサーによるものです。単一のスコープのみを考慮しているため、スコープ ルールは問題とはの関係もありません。解析段階で、パーサーはローカル変数が表示されるソースの場所を決定します。この表示は実行中に変更されません。

コード内の任意のポイントでローカル変数が定義されている (つまり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]ます。
  • nしかし、2 回目の反復 (がの場合1) では 細心の注意を払う必要があります。
    • 最初dest_arrは未定義です!
    • しかし、コードがelseケースに到達する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'.

の本体の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 メソッドが「マスク解除」/「シャドウ解除」されることを除いて、メソッドは括弧なしで呼び出すことができます。ただし、代入後のコードにはローカル変数が表示されます。

これらすべてから導き出せるいくつかのベストプラクティス

  • 同じスコープ内のメソッド名と同じ名前をローカル変数に付けないでください
  • ローカル変数の初期代入をwhileorforループの本体に入れたり、スコープ内で実行をジャンプさせたりするものを入れないでください (ラムダを呼び出したり、Continuation#callこれを行うこともできます)。割り当てをループの前に置きます。
于 2014-01-22T23:52:36.833 に答える
7

これはメッセージがループ内で定義されているためだと思います。ループ反復の最後に、「メッセージ」は範囲外になります。ループの外側で「メッセージ」を定義すると、各ループ反復の終わりに変数が範囲外になるのを防ぎます。だから私はあなたが正しい答えを持っていると思います。

私の提案が正しいかどうかをテストするために、各ループ反復の最初にメッセージの値を出力できます。

于 2009-10-31T15:07:44.920 に答える
2

なぜこれがバグだと思いますか?インタープリターは、特定のコードの実行時にメッセージが未定義である可能性があることを伝えています。

于 2009-10-31T15:09:52.060 に答える
2

なぜあなたが驚いたのかわかりません: 5 行目 (このmessage = nil行がないと仮定して) で、インタープリターがこれまで聞いたことのない変数を使用している可能性があります。インタプリタは「What's message? It's not a method I know, it's not a variable I know, it's not a keyword...」と言い、エラー メッセージが表示されます。

ここに、私が何を意味するかを示す簡単な例を示します。

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場合、最後に to を追加するnilことになります。これにより、次のエラーが発生します。

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 に答える