web-dev-qa-db-ja.com

クロージャでの変数キャプチャの詳細な説明

変数キャプチャがクロージャを作成するために変数をどのように取り込むかについての投稿を数え切れないほど見ましたが、それらはすべて特定の詳細が不足しているようで、全体を「コンパイラマジック」と呼んでいます。

私は次の明確な説明を探しています:

  1. ローカル変数の取得方法実際にはキャプチャされます。
  2. 値型と参照型のキャプチャの違い(ある場合)。
  3. そして、値型に関して発生しているボクシングがあるかどうか。

私の好みは、値とポインター(内部で起こることの中心に近い)の観点からの答えですが、値と参照を含む明確な答えも受け入れます。

58
DuckMaestro
  1. トリッキーです。すぐにそれに来るでしょう。
  2. 違いはありません。どちらの場合も、キャプチャされるのは変数自体です。
  3. いいえ、ボクシングは発生しません。

例を使用してキャプチャがどのように機能するかを示すのがおそらく最も簡単です...

単一の変数をキャプチャするラムダ式を使用するコードを次に示します。

using System;

class Test
{
    static void Main()
    {
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    }

    static Action CreateShowAndIncrementAction()
    {
        Random rng = new Random();
        int counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: {0}", counter);
        return () =>
        {
            Console.WriteLine(counter);
            counter++;
        };
    }
}

これがコンパイラーが行っていることです。ただし、C#では実際には発生し得ない「言いようのない」名前を使用する点が異なります。

using System;

class Test
{
    static void Main()
    {
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    }

    static Action CreateShowAndIncrementAction()
    {
        ActionHelper helper = new ActionHelper();        
        Random rng = new Random();
        helper.counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: {0}", helper.counter);

        // Converts method group to a delegate, whose target will be a
        // reference to the instance of ActionHelper
        return helper.DoAction;
    }

    class ActionHelper
    {
        // Just for simplicity, make it public. I don't know if the
        // C# compiler really does.
        public int counter;

        public void DoAction()
        {
            Console.WriteLine(counter);
            counter++;
        }
    }
}

ループで宣言された変数をキャプチャすると、ループの反復ごとにActionHelperの新しいインスタンスが作成されるため、変数のさまざまな「インスタンス」を効果的にキャプチャできます。

さまざまなスコープから変数をキャプチャすると、さらに複雑になります...そのような詳細レベルが本当に必要な場合、またはコードを記述してReflectorで逆コンパイルし、それに従うことができるかどうかをお知らせください:)

方法に注意してください:

  • ボクシングは関係ありません
  • 関係するポインタやその他の安全でないコードはありません

編集:これは、変数を共有する2人のデリゲートの例です。 1つのデリゲートはcounterの現在の値を示し、もう1つのデリゲートはそれをインクリメントします。

using System;

class Program
{
    static void Main(string[] args)
    {
        var Tuple = CreateShowAndIncrementActions();
        var show = Tuple.Item1;
        var increment = Tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    }

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    {
        int counter = 0;
        Action show = () => { Console.WriteLine(counter); };
        Action increment = () => { counter++; };
        return Tuple.Create(show, increment);
    }
}

...そして拡張:

using System;

class Program
{
    static void Main(string[] args)
    {
        var Tuple = CreateShowAndIncrementActions();
        var show = Tuple.Item1;
        var increment = Tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    }

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    {
        ActionHelper helper = new ActionHelper();
        helper.counter = 0;
        Action show = helper.Show;
        Action increment = helper.Increment;
        return Tuple.Create(show, increment);
    }

    class ActionHelper
    {
        public int counter;

        public void Show()
        {
            Console.WriteLine(counter);
        }

        public void Increment()
        {
            counter++;
        }
    }
}
79
Jon Skeet