web-dev-qa-db-ja.com

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

C#でラムダ式または無名メソッドを使用するときは、 修正クロージャへのアクセス pitfallに注意する必要があります。例えば:

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

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

here で説明したように、これは上記の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ループを使ってできることはありますか。それらが内部スコープの変数でコンパイルされている場合は不可能でしたか。これは、無名メソッドやラムダ式が使用可能または一般的になる前に行われた任意選択です。それ以来、改訂されていませんか?

1591

コンパイラーは、検出およびデバッグが困難な場合が多いエラーが発生しやすい方法で変数を宣言しますが、認識できるメリットはありません。

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

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

有害と見なされるループ変数のクローズ

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

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

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

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

1368
Eric Lippert

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

私にとって、最も説得力のある議論は、各反復で新しい変数を持つことはfor(;;)スタイルループと矛盾するということです。 for (int i = 0; i < 10; i++)の各反復で新しいint 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変数のクロージャー

182
Krizz

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

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.

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

98
Godeke

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
  }
}

(...)

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

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

vがwhileループの外側で宣言されている場合、それはすべての繰り返しで共有され、forループの後の値は最終値13になります。これがfの呼び出しによって出力されます。代わりに、それぞれの繰り返しがそれ自身の変数vを持っているので、最初の繰り返しでfによって捕獲されたものは値7を保持し続け、それが表示されます。 ( 注:C#の以前のバージョンでは、whileループの外側でvを宣言していました。

57
Paolo Moretti

私の意見では、それは奇妙な質問です。コンパイラがどのように動作するのかを知っておくのは良いことですが、それは「知っておくこと」だけです。

あなたがコンパイラのアルゴリズムに依存するコードを書くならば、それは悪い習慣です。そして、この依存関係を排除するためにコードを書き直すことはより良いです。

これは就職の面接にはよい質問です。しかし、実生活では、就職の面接で解決した問題に直面したことはありません。

Foreachの90%がコレクションの各要素の処理に使用します(一部の値の選択または計算には使用されません)。ループ内で値を計算する必要がある場合もありますが、BIGループを作成するのはお勧めできません。

値を計算するにはLINQ式を使用することをお勧めします。ループ内で多くのことを計算するとき、あなた(または他の誰か)がこのコードを読む2〜3か月後には、これが何であり、それがどのように機能するのか理解できないでしょう。

0
TemaTre