web-dev-qa-db-ja.com

C#のループでキャプチャされた変数

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つの変数を参照するすべてのアクションによるものと思われます。その結果、呼び出されると、すべて同じ出力が得られます。

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

194
Morgan Cheng

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

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を参照してください。また、私の クロージャーに関する記事 にも他の例があります。

173
Jon Skeet

あなたが経験しているのは、クロージャーと呼ばれるものだと思います http://en.wikipedia.org/wiki/Closure_(computer_science) 。 Lambaには、関数自体のスコープ外の変数への参照があります。あなたのランバはあなたがそれを呼び出すまで解釈されず、一度それが実行時に変数が持っている値を取得します。

21
TheCodeJunkie

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

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

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

10
gerrard00

これを回避する方法は、必要な値をプロキシ変数に保存し、その変数をキャプチャさせることです。

I.E.

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

はい、ループ内で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();
5
cfeduke

マルチスレッド(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();
}
5
Sunil

これはループとは関係ありません。

ラムダ式() => variable * 2を使用するため、この動作がトリガーされます。外側のスコープvariableは、ラムダの内側のスコープで実際に定義されていません。

ラムダ式(C#3 +、およびC#2の匿名メソッド)は実際のメソッドを作成します。これらのメソッドに変数を渡すには、いくつかのジレンマが伴います(値渡し?参照渡し?C#は参照渡しになります-しかし、これにより、参照が実際の変数よりも長持ちする別の問題が発生します)。これらすべてのジレンマを解決するためにC#が行うことは、ラムダ式で使用されるローカル変数に対応するフィールドと実際のラムダメソッドに対応するメソッドを持つ新しいヘルパークラス(「クロージャ」)を作成することです。コードのvariableへの変更は、実際にはそのClosureClass.variableの変更に変換されます

したがって、whileループはClosureClass.variableを10に達するまで更新し続け、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;
}

いずれにせよ、"Closures"はループに関連した概念ではありませんではなく、匿名メソッド/ローカルスコープ変数のラムダ式の使用-ループのいくつかの不注意な使用はクロージャートラップを示しています。

4
David Refaeli

これはクロージャー問題と呼ばれ、単純にコピー変数を使用するだけで済みます。

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());
}
0