261

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 を出力するはずですが、実際には 10 が 5 個出力されます。

キャプチャされた 1 つの変数を参照するすべてのアクションが原因のようです。その結果、それらが呼び出されると、すべて同じ出力が得られます。

この制限を回避して、各アクション インスタンスに独自のキャプチャ変数を持たせる方法はありますか?

4

10 に答える 10

239

はい - ループ内で変数のコピーを取得します。

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

これは、C# コンパイラが変数宣言に到達するたびに "新しい" ローカル変数を作成するかのように考えることができます。実際、適切な新しいクロージャーオブジェクトを作成し、複数のスコープで変数を参照すると(実装に関して)複雑になりますが、機能します:)

この問題のより一般的な発生は、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 を参照してください。また、クロージャに関する私の記事には、さらに多くの例があります。

C# 5 コンパイラ以降 (以前のバージョンの C# を指定した場合でも) の動作がforeach変更されたため、ローカル コピーを作成する必要がなくなったことに注意してください。詳細については、この回答を参照してください。

于 2008-11-07T07:32:04.583 に答える
25

あなたが経験していることは、閉鎖http://en.wikipedia.org/wiki/Closure_(computer_science)として知られているものだと思います。あなたのランバには、関数自体の外側にある変数への参照があります。あなたのランバはあなたがそれを呼び出すまで解釈されず、実行時に変数が持っている値を取得します。

于 2008-11-07T07:34:36.887 に答える
14

舞台裏では、コンパイラはメソッド呼び出しのクロージャーを表すクラスを生成しています。ループの反復ごとに、クロージャー クラスの単一のインスタンスを使用します。コードは次のようになります。これにより、バグが発生する理由を簡単に確認できます。

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

これは実際にはサンプルからコンパイルされたコードではありませんが、私自身のコードを調べたところ、コンパイラが実際に生成するものと非常によく似ています。

于 2013-03-29T16:49:55.013 に答える
9

これを回避するには、必要な値をプロキシ変数に格納し、その変数を取得します。

IE

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}
于 2008-11-07T07:33:18.607 に答える
5

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();
于 2008-11-07T07:32:55.470 に答える
4

同じ状況がマルチスレッド (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();
}
于 2011-01-28T14:00:53.497 に答える