C#のループでキャプチャされた変数
-
06-07-2019 - |
質問
C#に関する興味深い問題に出会いました。以下のようなコードがあります。
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => variable * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
0、2、4、6、8を出力するはずですが、実際には5つの10を出力します。
それは、キャプチャされた1つの変数を参照するすべてのアクションによるものと思われます。その結果、それらが呼び出されると、すべて同じ出力になります。
この制限を回避して、各アクションインスタンスに独自のキャプチャ変数を持たせる方法はありますか?
解決
はい-ループ内で変数のコピーを取得します:
while (variable < 5)
{
int copy = variable;
actions.Add(() => copy * 2);
++ variable;
}
C#コンパイラが&quot; new&quot;を作成するかのように考えることができます。ローカル変数が変数宣言に到達するたびに。実際、適切な新しいクロージャオブジェクトを作成し、複数のスコープの変数を参照すると(実装の点で)複雑になりますが、動作します:)
この問題のより一般的な発生は、 for
または foreach
を使用していることに注意してください:
for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud
この詳細については、C#3.0仕様のセクション7.14.4.2を参照してください。また、私の記事クロージャにはさらに例があります。
他のヒント
あなたが経験しているのは、クロージャー http://en.wikipediaとして知られているものだと思います。 org / wiki / Closure_(computer_science)。 Lambaには、関数自体の範囲外の変数への参照があります。あなたのランバはあなたがそれを呼び出すまで解釈されず、一度それが実行時に変数が持っている値を取得します。
舞台裏では、コンパイラはメソッド呼び出しのクロージャを表すクラスを生成しています。ループの反復ごとに、クロージャークラスの単一インスタンスを使用します。コードは次のようになります。これにより、バグが発生する理由を簡単に確認できます。
void Main()
{
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
var closure = new CompilerGeneratedClosure();
Func<int> anonymousMethodAction = null;
while (closure.variable < 5)
{
if(anonymousMethodAction == null)
anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);
//we're re-adding the same function
actions.Add(anonymousMethodAction);
++closure.variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
}
class CompilerGeneratedClosure
{
public int variable;
public int YourAnonymousMethod()
{
return this.variable * 2;
}
}
これは実際にサンプルからコンパイルされたコードではありませんが、私は自分のコードを調べましたが、これはコンパイラが実際に生成するものと非常によく似ています。
これを回避する方法は、必要な値をプロキシ変数に保存し、その変数をキャプチャさせることです。
I.E。
while( variable < 5 )
{
int copy = variable;
actions.Add( () => copy * 2 );
++variable;
}
はい、ループ内で variable
をスコープし、その方法でラムダに渡す必要があります:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
int variable1 = variable;
actions.Add(() => variable1 * 2);
++variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Console.ReadLine();
マルチスレッドでも同じ状況が発生しています(C#、 .NET 4.0]。
次のコードを参照してください:
目的は、1,2,3,4,5を順番に印刷することです。
for (int counter = 1; counter <= 5; counter++)
{
new Thread (() => Console.Write (counter)).Start();
}
出力は興味深いです! (21334のようになります...)
唯一の解決策は、ローカル変数を使用することです。
for (int counter = 1; counter <= 5; counter++)
{
int localVar= counter;
new Thread (() => Console.Write (localVar)).Start();
}
これはループとは関係ありません。
ラムダ式()=&gt;を使用するため、この動作がトリガーされます。変数* 2
。ここで、外側のスコープ variable
は、ラムダの内側のスコープで実際に定義されていません。
ラムダ式(C#3 +およびC#2の匿名メソッド)は、実際のメソッドを作成します。これらのメソッドへの変数の受け渡しには、いくつかのジレンマが伴います(値渡し?参照渡し?C#は参照渡しになります-しかし、これにより、参照が実際の変数よりも長持ちする別の問題が発生します)。これらすべてのジレンマを解決するためにC#が行うことは、ラムダ式で使用されるローカル変数に対応するフィールドと、実際のラムダメソッドに対応するメソッドを持つ新しいヘルパークラス(「クロージャ」)を作成することです。コード内の variable
への変更は、実際にはその ClosureClass.variable
したがって、whileループは、10に達するまで ClosureClass.variable
を更新し続けるため、forループはすべて同じ ClosureClass.variable
で動作するアクションを実行します。
期待する結果を得るには、ループ変数と閉じられる変数との間に分離を作成する必要があります。これを行うには、別の変数を導入します。例:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
actions.Add(() => t * 2);
++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
クロージャーを別のメソッドに移動して、この分離を作成することもできます:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(Mult(variable));
++variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Multをラムダ式として実装できます(暗黙のクロージャ)
static Func<int> Mult(int i)
{
return () => i * 2;
}
または実際のヘルパークラス:
public class Helper
{
public int _i;
public Helper(int i)
{
_i = i;
}
public int Method()
{
return _i * 2;
}
}
static Func<int> Mult(int i)
{
Helper help = new Helper(i);
return help.Method;
}
いずれの場合でも、&quot; Closures&quot;ループに関連する概念ではなくではなく、ローカルスコープ変数の匿名メソッド/ラムダ式使用に関連しています-ただし、ループのいくつかの不注意な使用はクロージャートラップを示しています。
これは閉包問題と呼ばれ、 コピー変数を使用するだけで完了です。
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
int i = variable;
actions.Add(() => i * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}