C#がforeachで変数を再利用する理由はありますか?


1685

C#でラムダ式または無名メソッドを使用する場合、変更されたクロージャーの落とし穴へのアクセスに注意する必要があります。例えば:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

変更されたクロージャのため、上記のコードではWhere、クエリのすべての句がの最終値に基づいていますs

ここで説明したように、これsは、foreach上記のループで宣言された変数がコンパイラで次のように変換されるために発生します。

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

このようにする代わりに:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

ここで指摘したように、ループの外側で変数を宣言してもパフォーマンス上の利点はありません。通常の状況では、これを行うために考えられる唯一の理由は、ループのスコープ外で変数を使用する場合です。

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

ただし、foreachループで定義された変数はループの外では使用できません。

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

そのため、コンパイラーは変数を宣言しますが、多くの場合、発見やデバッグが難しいエラーが発生しやすく、認識できる利点はありません。

foreachこの方法でループを使用してループを実行できる場合、それらが内部スコープ変数を使用してコンパイルされた場合は不可能でしたか、それとも、匿名メソッドとラムダ式が利用可能または一般的になる前に行われた任意の選択であり、それ以来改訂されていませんか?


4
何が問題になっていString s; foreach (s in strings) { ... }ますか?
ブラッドクリスティー

5
@BradChristie OPは本当は話してforeachいませんが、ラムダ式について話しているため、OPで示されているのと同様のコードが生成されます...
Yahia

22
@BradChristie:コンパイルできますか?(エラー:タイプと識別子の両方がforeachステートメント必要です)
オースティンサローネン

32
@JakobBotschNielsen:ラムダの閉じた外側ローカルです。なぜそれがスタック上にあると想定しているのですか?スタックフレームより寿命が長い
Eric Lippert、2012年

3
@EricLippert:私は混乱しています。ラムダがforeach変数(ループの外側で内部的に宣言されている)への参照をキャプチャするため、最終的には最終値と比較することになることを理解しています。私が得ること。私が理解していないのは、ループ内で変数宣言するとどのように違いが生じるかです。コンパイラライターの観点からは、宣言がループの内側か外側かに関係なく、スタックに文字列参照(var 's')を1つだけ割り当てています。反復ごとに新しい参照をスタックにプッシュしたくありません。
アンソニー

回答:


1407

コンパイラーは変数を宣言しますが、多くの場合、発見やデバッグが難しいエラーが発生しやすくなりますが、認識できる利点はありません。

あなたの批判は完全に正当化されます。

この問題については、ここで詳しく説明します。

有害と見なされるループ変数を閉じる

この方法でforeachループを使用して、内部スコープの変数を使用してコンパイルした場合には実行できないことはありますか?または、これは匿名メソッドとラムダ式が利用可能になるか一般的になる前に行われた任意の選択であり、それ以降は変更されていませんか?

後者。C#1.0仕様では、実際にはループ変数がループ本体の内部にあるのか外部にあるのかはわかりませんでした。C#2.0でクロージャーセマンティクスが導入されたとき、「for」ループと一貫性のあるループ変数をループの外に置くように選択されました。

私はすべての決定を後悔していると言っても差し支えないと思います。これはC#での最悪の「落とし穴」の1つであり、これを修正するために重大な変更を行います。C#5では、foreachループ変数は論理的ループの本体内にあるため、クロージャーは毎回新しいコピーを取得します。

forループは変更されませんし、変更がC#の以前のバージョンに「背中が移植」されることはありません。したがって、このイディオムを使用する場合は引き続き注意が必要です。


177
実際、C#3とC#4でこの変更を押し戻しました。C#3を設計したとき、ラムダ(およびクエリLINQのおかげでforeachループでの理解(変装のラムダ)。私たちは、むしろC#3でそれを修正するよりも、そう遅く、それを固定正当化するのに十分に悪い取得する問題を待っていたことを後悔
エリックリペット

75
そして今、私たちforeachは「安全」であることを覚えておく必要がありますが、安全でforはありません。
leppie 2012年

22
@michielvoo:この変更は、下位互換性がないという意味で壊れています。古いコンパイラを使用すると、新しいコードは正しく実行されません。
leppie 2012年

41
@ベンジョール:いいえ、それが私たちがそれを受け入れようとする理由です。Jon Skeetは私に重要な重大な変更シナリオを指摘しました。それは、誰かがC#5でコードを記述してテストし、それをまだC#4を使用している人々と共有しているということです。うまくいけば、そのようなシナリオの影響を受ける人々の数は少ないです。
Eric Lippert、2012年

29
余談ですが、ReSharperはこれを常にキャッチし、「変更されたクロージャーへのアクセス」として報告します。次に、Alt + Enterを押すと、コードが自動的に修正されます。jetbrains.com/resharper
マイクチェンバレン

191

あなたが求めていることは、有害である考えられているループ変数のクローズとその続編である彼のブログ投稿のEric Lippertによって完全にカバーされています

私にとって、最も説得力のある引数は、各反復で新しい変数を使用することは、for(;;)スタイルループと矛盾することです。のint i各イテレーションで新しいを期待しfor (int i = 0; i < 10; i++)ますか?

この動作の最も一般的な問題は、反復変数のクロージャーを作成することであり、簡単な回避策があります。

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

この問題に関する私のブログ投稿:C#のforeach変数に対するクロージャー


18
結局のところ、これを書いているときに人々が実際に望んでいるのは、複数の変数を持つことではなく、を閉じることです。そして、一般的なケースでは、そのために使用できる構文を考えるのは難しいです。
Random832

1
はい、値で閉じることはできませんが、回答を編集して含めた非常に簡単な回避策があります。
Krizz

6
参照よりもC#でのクロージャのクローズが悪すぎる。デフォルトで値をクローズする場合は、を使用する代わりに、変数のクローズを簡単に指定できますref
Sean U

2
@Krizz、これは強制された一貫性が一貫性よりも有害であるケースです。変更されたクロージャの問題(私など)へのアクセスについて知る前に問題にぶつかった人の数を考えると、forループではなくforeachを使用する場合、人々は期待どおりに「正しく機能する」はずです。 。
アンディ

2
@ Random832はC#については知りませんが、Common LISPにはその構文があり、可変変数とクロージャーを持つすべての言語にもC#がある(そうでなければならない)ことを示しています。変化する場所への参照をクローズするか、特定の瞬間にそれが持っている値(クロージャーの作成)をクローズします。これは、PythonとSchemeの同様のもの(cutrefs / varsおよび部分的に評価されたクロージャーに評価cuteを保持するため)について説明します。
ネスは

103

これに噛まれて、私はローカルで定義された変数を、最も内側のスコープに含めて、クロージャーに転送する習慣を持っています。あなたの例では:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

私がやります:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

いったんその習慣を身につければ、外側のスコープにバインドすることを実際に意図した非常にまれなケースでそれを回避できます。正直なところ、これまでにそうしたことはないと思います。


24
それは典型的な回避策です貢献してくれてありがとう。Resharperは、このパターンを認識して注目を集めるのに十分スマートです。これは素晴らしいことです。私はしばらくこのパターンに少しも触れていませんが、エリック・リッパートの言葉では、「私たちが受け取る最も一般的な不正なバグレポートは1つだけ」なので、それを回避する方法以上の理由を知りたいと思っていました
StriplingWarrior

62

C#5.0では、この問題は修正されており、ループ変数を閉じて、期待する結果を得ることができます。

言語仕様によれば:

8.8.4 foreachステートメント

(...)

フォームのforeachステートメント

foreach (V v in x) embedded-statement

次に、次のように展開されます。

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

(...)

vwhileループ内に配置することは、埋め込みステートメントで発生する匿名関数によってキャプチャされる方法にとって重要です。例えば:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

vwhileループの外で宣言された場合、それはすべての反復間で共有され、forループの後のその値13は、の呼び出しが出力する最終値にfなります。代わりに、各反復には独自の変数があるため、最初の反復でvによってキャプチャさfれた変数は、出力される値を保持し続け 7ます。(注:以前のバージョンのC#vは、whileループの外側で宣言されています。


1
なぜこの初期バージョンのC#は、whileループ内でvを宣言したのですか?msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang 2013

4
@colinfang エリックの回答を必ずお読みください:C#1.0仕様(リンクでVS 2003について話している、つまりC#1.2)は、実際にループ変数がループ本体の内部にあるか外部にあるかを示していませんでした。。C#2.0でクロージャーセマンティクスが導入されたとき、「for」ループと一貫性のあるループ変数をループの外側に配置する選択が行われました。
パオロモレッティ

1
それで、あなたはリンクの例がその時の決定的な仕様ではなかったと言っているのですか?
colinfang 2013

4
@colinfang決定的な仕様でした。問題は、後で(C#2.0で)導入された機能(つまり、関数クロージャー)について話していることです。C#2.0が登場したとき、彼らはループ変数をループの外に置くことに決めました。そして、彼らがC#5.0で再び考えを変えたより:)
Paolo Moretti
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.