2009-11-17 18 views
8

Esta es una pregunta basada en el artículo "Closing over the loop variable considered harmful" por Eric Lippert.
Es una buena lectura, Eric explica por qué después de esta pieza de código todos funcs devolverán el último valor en v:Cómo | ¿Dónde se almacenan las variables cerradas?

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

Y la versión correcta parece:

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

Ahora mi pregunta es cómo y dónde están almacenadas esas variables 'v2' capturadas. En mi comprensión de la pila, todas esas variables v2 ocuparía la misma pieza de memoria.

Mi primer pensamiento fue el boxeo, cada miembro de la función manteniendo una referencia a un v2 en caja. Pero eso no explicaría el primer caso.

+0

OK, después de leer mi propia pregunta, supongo que se explicaría por: en la primera versión, 'v' está encuadrada una vez y la referencia se vuelve a utilizar. Pero me gustaría ver una respuesta más autorizada. –

Respuesta

6

Normalmente la variable v2 tiene asignado un espacio en la pila al comienzo del bloque de código en el que se encuentra. Al final del bloque de código (es decir, el final de la iteración) la pila se enrolla (I ' m describiendo el escenario lógico no un comportamiento real optimizado). Por lo tanto, cada v2 es en efecto un v2 diferente de la iteración anterior, aunque es cierto que terminaría ocupando la misma ubicación de memoria.

Sin embargo, el compilador detecta que v2 está siendo utilizado por una función anónima creada por la lambda. Lo que hace el compilador es hoist la variable v2. El compilador crea una nueva clase que tiene un campo Int32 para contener el valor de v2, no se le asigna un lugar en la pila. También hace que la función anónima sea un método de este nuevo tipo. (Por simplicidad le daré un nombre a esta clase sin nombre, vamos a llamarlo "Cosa").

Ahora en cada iteración una nueva instancia de "cosa" es creado y cuando se le asigna su v2 el campo Int32 que se asigna realmente no sólo un punto en la memoria de pila. La expresión de función anónima (lambda) ahora devolverá un delegado que tiene referencia de objeto de instancia no nula, esta referencia será a la instancia actual de "Thing".

Cuando se invoca al delegado para la función anónima, se ejecutará como un método de instancia de una instancia de "Cosa". Por lo tanto, v2 está disponible como un campo miembro y tendrá el valor otorgado durante la iteración en que se creó esta instancia de "Cosa".

4

Además de las respuestas de Neil y Anthony, aquí hay un ejemplo del código que podría generarse automáticamente en ambos casos.

(Tenga en cuenta que esto es sólo para demostrar el principio, el código real generado por el compilador no se verá exactamente como esta. Si desea ver el código real, entonces se puede echar un vistazo usando reflector.)

// 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 Agradable y simple. – Groo

Cuestiones relacionadas