他のいくつかの回答とは対照的に、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
次のいずれかになるまで左矢印を押し続けます。
- 現在のスコープの先頭に到達する (これがいつ発生するかを知るには、Ruby のスコープ規則を理解する必要があります)
- 割り当てるリーチコード
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
メソッドが「マスク解除」/「シャドウ解除」されることを除いて、メソッドは括弧なしで呼び出すことができます。ただし、代入後のコードにはローカル変数が表示されます。
これらすべてから導き出せるいくつかのベストプラクティス
- 同じスコープ内のメソッド名と同じ名前をローカル変数に付けないでください
- ローカル変数の初期代入を
while
orfor
ループの本体に入れたり、スコープ内で実行をジャンプさせたりするものを入れないでください (ラムダを呼び出したり、Continuation#call
これを行うこともできます)。割り当てをループの前に置きます。