匿名委任変数によって取得されたトラップ


簡単な例です.
 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             for (var i = 0; i < 3; i++)
 6             {
 7                 Task.Factory.StartNew(() => Func1(i));
 8             }
 9 
10             Console.ReadKey();
11         }
12 
13         static void Func1(int i)
14         {
15             Console.WriteLine(i);
16         }
17     }

結果は0 1 2ではなく3 3 3
(補足:サイクル数が大きくなると、出力は同じではありませんが、まだ予想されていません)
 
説明:
匿名関数には、変数をキャプチャする特性があります.
閉パケットは、自由(バインドされていない)変数を含むことができるコードブロックである.これらの変数は、このコードブロックまたは任意のグローバルコンテキストで定義されるのではなく、コードブロックを定義する環境で定義されます.
 
参考資料:
簡単に言えば、パケットを閉じると、いくつかの動作をカプセル化し、オブジェクトのように渡すことができ、元の最初の宣言時のコンテキストにアクセスすることができます.これにより、制御構造、論理操作などを呼び出しの詳細から分離することができる.元のコンテキストにアクセスする能力は、実装にいくつかのコンパイラテクニックが追加されているにもかかわらず、一般的なオブジェクトを区別するための閉パッケージの重要な特徴です.
匿名メソッドまたはlambdaでは、匿名の定義範囲内の変数にアクセスまたは変更できることを知っています.例:
int num = 1;   
Func incNum = () => ++num; 
ここでlambda式は、その外部で定義された変数numを使用します.このセグメントlambda文ブロックは外部変数numをキャプチャする閉パケットを構成すると考えられる.
まあ、そんなに見ていてつらい定義の話は言わないでください.本題に入り、C#で変数がどのようにキャプチャされているかを見てみましょう.例を見てみましょう
public Func CreateFunction()   
{   
String str=「私のラッキーな数字は」;   
int num = 17;   
Func func = () => str + num;   
return func;   

この例では、関数を返す方法CreateFunctionを定義します.返される関数は、Stringタイプのstrとintタイプのnumの2つの変数をキャプチャする閉パケットを構成します.
では、この関数を使用できます.
Func   
myFunc = CreateFunction();   
String result = myFunc();  
この2行のコードが実際に何をしたのか分析してみましょう.最初の行は分かりやすく,メソッドCreateFunctionによって生成された匿名関数を依頼myFuncに付与した.
2行目はmyFuncを実行し、戻り結果を変数resultに割り当てたことをよりよく理解します.さらに考えてみましょう.myFuncを実行すると、CreateFunctionで2つの変数strとnumを定義することにアクセスします.
このときCreateFunctionのスタックフレームはとっくに破棄されており、その内部定義の変数は今でも「生死不明」になっているが、この2つの変数が閉パッケージで捕獲されたことを知っているので、この2つの変数はこれまでもアクセスできると信じている.
strオブジェクトの場合、リファレンスタイプであるため、リファレンスが保存されているものがある限り、破棄されません.これにより、私たちがそれを必要とするとき、コンパイラや実行時に失われたことを教えてくれる心配はありません.
しかしnumでは状況が少し違います.numは値タイプです.値タイプがスタックに存在することを知っています.存在するスタックフレーム(つまりCreateFunctionのフレーム)はCreateFunctionの実行が完了すると破棄され、その上に存在する任意の値タイプも破棄されることを知っています.もちろん、私たちが注目している変数numを含んでいます.
では、なぜnumに安全にアクセスできるのでしょうか.C#の変数キャプチャメカニズムには、値タイプに通常の生存サイクルに違反する不思議な点があるのだろうか.箱詰め!各値タイプを1つのオブジェクトに組み込むと、この値タイプにそれを包むオブジェクトと同じ寿命を持たせることができるとすぐに思います.
しかし、これはC#実装者が選択した方法ではありません!C#は、取得する必要がある値タイプの変数ごとに箱詰め操作を行うのではなく、すべての取得した変数を同じ大きな「箱」に配置します.コンパイラが変数の取得を必要とする場合、暗黙的にバックグラウンドにタイプを構築します.このタイプには、各閉パッケージで取得した変数(値タイプの変数と参照タイプの変数を含む)が共通のフィールドとして含まれています.これで、コンパイラは
匿名関数やlambda式に現れる外部変数を維持します.
さらに、ILDASMツールを使用してCreateFunctionメソッドのILコードを表示すると、コンパイラのルートにnumとstr変数が宣言されていないことがわかります.代わりに、タイプ名とインスタンス名の両方が醜いパッケージオブジェクトであることを宣言します.これは、上述したコンパイラによって黙々と生成され、取得変数の参照をすべて保存したオブジェクトです.
また、CreateFunctionメソッドでは、C#ソースコード内のstrおよびnumに対するすべての操作が、ILにおいてパッケージオブジェクトの同名の共有メンバーに対する操作に変換されることも見られます.ちなみに、私たちが作ったlambda式「()=>str+num」も今ではコンパイラによってこのパッケージオブジェクトに変換されています!