2009-11-17 8 views
8

これは、Eric Lippertの記事"Closing over the loop variable considered harmful"に基づく質問です。How?クローズオーバー変数はどこに格納されていますか?

var funcs = new List<Func<int>>(); 
foreach (var v in values) 
{ 
    funcs.Add(() => v); 
} 

をし、正しいバージョンは次のようになります。:今すぐ

foreach (var v in values) 
{ 
    int v2 = v; 
    funcs.Add(() => v2); 
} 


エリックは、コードのこの部分の後にすべてのfuncsをvに最後値を返しますなぜ良いの読み取りを説明しています私の質問は、どのようにして 'v2'の変数が保存されているのかということです。スタックの私の理解では、それらのすべてのv2変数は、同じメモリを占有するでしょう。

私の最初の考えはボクシングでした、各funcメンバーはボックス化されたv2への参照を保持していました。しかし、それは最初のケースを説明しません。

+0

OK、私自身の質問を読んだ後、私はそれが最初のバージョンでは 'v'が1回boxedされ、リファレンスが再利用されることで説明されると思います。しかし、もっと権威ある答えを見たいと思っています。 –

答えて

6

通常、変数v2には、見つかったコードブロックの先頭にあるスペースが割り当てられます。コードブロックの最後(つまり、反復の終わり)でスタックは巻き戻されます(I '論理シナリオを最適化された実際の行動ではなく記述している)。したがって、各v2は、実質的に前回の反復とは異なるv2ですが、最終的には同じメモリ位置を占めることになります。

しかし、コンパイラは、v2がラムダによって作成された無名関数によって使用されていることを示します。コンパイラが行うのはhoistv2という変数です。コンパイラは、v2の値を保持するInt32フィールドを持つ新しいクラスを作成しますが、スタック上の場所は割り当てられません。また、匿名関数をこの新しい型のメソッドにします。 (わかりやすくするために、この名前のないクラスに名前をつけて、それを「もの」と呼ぶことができます)。 v2が実際にスタックメモリ内だけでなく、ポイントが付与されてのInt32フィールドが割り当てられたときに「シング」ので今

反復新しいインスタンスが作成されます。匿名関数式(ラムダ)はnull以外のインスタンスオブジェクト参照を持つデリゲートを返します。この参照は現在のインスタンス "Thing"になります。

匿名関数の代理人が呼び出されると、 "Thing"インスタンスのインスタンスメソッドとして実行されます。したがって、v2はメンバフィールドとして利用可能であり、 "Thing"のこのインスタンスが作成された反復中に値を与えます。

4

NeilとAnthonyの回答に加えて、ここでは両方のケースで自動生成されるコードの例を示します。

(これは原理を実証するだけであることに注意してください、実際のコンパイラによって生成されたコードは、まさにこのようにしていません。あなたが実際のコードを見たいなら、あなたはリフレクターを使用して見てみることができます。)

// first loop 
var captures = new Captures(); 
foreach (var v in values) 
{ 
    captures.Value = v; 
    funcs.Add(captures.Function); 
} 

// second loop 
foreach (var v in values) 
{ 
    var captures = new Captures(); 
    captures.Value = v; 
    funcs.Add(captures.Function); 
} 

// ... 

private class Captures 
{ 
    public int Value; 

    public int Function() 
    { 
     return Value; 
    } 
} 
+0

+1素敵でシンプル。 – Groo

関連する問題