2010-11-11 6 views
11

dado:C# lambda, valor de variable local no tomado cuando piensas?

void AFunction() 
{ 

    foreach(AClass i in AClassCollection) 
    { 
     listOfLambdaFunctions.AddLast(() => { PrintLine(i.name); } ); 
    } 
} 

void Main() 
{ 
    AFunction(); 
    foreach(var i in listOfLambdaFunctions) 
     i(); 
} 

ahora se podría pensar que harían lo equivilant a:

void Main() 
{ 

    foreach(AClass i in AClassCollection) 
     PrintLine(i.name); 
} 

pero no es así, lo que va a hacer es imprimir el nombre del último elemento de cada AClassCollection ¡hora! por lo que, básicamente, el mismo elemento se estaba utilizando en cada función lambda. sospechaba que podría haber algún retraso en "cuando se creó el lambda" o "cuando tomó una instantánea de las variables externas utilizadas en él", o básicamente, simplemente sosteniendo una 'referencia a la variable local' i '

así que hice esto:

string astr = "a string"; 
AFunc fnc =() => { System.Diagnostics.Debug.WriteLine(astr); }; 
astr = "chagnged!"; 
fnc(); 

y sorpresa, sorpresa, produce "¡cambiado!"

estoy usando XNA 3.1 (C# lo que es)

lo que está pasando? ¿la función lambda almacena de alguna manera una "referencia" a la variable o algo así? hay de todos modos alrededor de este problema?

+1

relacionadas: ¿Cómo http://stackoverflow.com/questions/271440/c-captured-variable-in-loop – Ani

+1

puede salir "cambiado"! cuando asignó "chapped!"? : p –

+0

"La regla de cierre sobre variables, no sobre valores" se aplica aquí, así que no espere que el valor de astr se copie en esa función lambda, copió la variable, en este caso es una referencia a esa variable, y ya lo has probado con tus propias observaciones. – Onur

Respuesta

17

Esto es un cierre modificado

Ver: preguntas similares como Access to Modified Closure

Para evitar el problema que tiene que guardar una copia de la variable dentro del alcance del bucle for:

foreach(AClass i in AClassCollection) 
    { 
     AClass anotherI= i; 
     listOfLambdaFunctions.AddLast(() => { PrintLine(anotherI.name); } ); 
    } 
+0

ahh sí! una solución tan obvia ahora me lo indica: D. Siempre olvido que técnicamente se crea y borra una variable como esa en cada ciclo (en C++) o simplemente se crea una nueva 'variable' en cada ciclo. principalmente porque hace que el código nazi óptimo dentro de mí llore solo de pensar en eso realmente sucediendo :), ¡sin duda útil aquí! – matt

+1

@matt, el código óptimo nazi debería relajarse un poco, después de considerar que el hecho de que tales variables se creen de nuevo cada ciclo no significa que tengan que implementarse como tales en la versión compilada si no es necesario. Al mismo tiempo, permite que lo anterior funcione. Lo mejor de ambos mundos. –

5

¿Qué está pasando? ¿la función lambda almacena de alguna manera una "referencia" a la variable o algo así?

exactamente eso; Las variables capturadas C# son variable, no valor de la variable. Generalmente, usted puede evitar esto mediante la introducción de una variable temporal y vinculante a que:

string astr = "a string"; 
var tmp = astr; 
AFunc fnc =() => { System.Diagnostics.Debug.WriteLine(tmp); }; 

especialmente en foreach donde esto es notorio.

2

Sí, la lambda almacena una referencia a la variable (conceptualmente hablando, de todos modos).

Una solución muy simple es la siguiente:

foreach(AClass i in AClassCollection) 
    { 
     AClass j = i; 
     listOfLambdaFunctions.AddLast(() => { PrintLine(j.name); } ); 
    } 

En cada iteración del bucle foreach, una nueva j se crea, que la captura de lambda. i por otro lado, es la misma variable en todo, pero se actualiza con cada iteración (para que todas las lambdas terminen viendo el último valor)

Y acepto que esto es un poco sorprendente.:)

1

También me atrapó esta, como dijo Calgary Coder, es un cierre modificado. Realmente tuve problemas para detectarlos hasta que me puse de nuevo. Como es una de las advertencias que resharper busca, estoy mucho mejor identificándolas a medida que codigo.

12

¿la función lambda almacena de alguna manera una "referencia" a la variable o algo así?

Cerrar. La función lambda captura la variable. No es necesario almacenar una referencia en una variable, y de hecho, en .NET es imposible almacenar permanentemente una referencia a una variable. Simplemente captura la variable completa . Nunca captura el valor de la variable.

Recuerde, una variable es una ubicación de almacenamiento. El nombre "i" se refiere a una ubicación de almacenamiento particular, y en su caso, siempre se refiere a la misma ubicación de almacenamiento .

¿Hay de todos modos alrededor de este problema?

Sí. Crea una nueva variable todo el tiempo a través del ciclo. El cierre luego captura una variable diferente cada vez.

Este es uno de los problemas más frecuentes de C#. Estamos considerando cambiar la semántica de la declaración de la variable de bucle para que se cree una nueva variable cada vez a través del bucle.

Para más detalles sobre este tema ver mis artículos sobre el tema:

http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/

+0

En cuanto a los cálculos de costo/beneficio a los que hable a veces Eric, ¿cómo valdría un cambio en esto más que decir, una corrección de error o una nueva característica? – asawyer

+0

@asawyer: esa es una buena pregunta. Este es probablemente el informe de error "falso" número uno que obtenemos. Confunde a mucha gente. El código que cierra una variable de bucle es quebradizo, y casi nadie en realidad * quiere * el comportamiento especificado. Y de todos modos vamos a realizar una cirugía mayor al código de cierre para hacer que el trabajo "aguarde"; podemos arreglar esto mientras estamos en eso. Esas son buenas razones para tomar medidas. Por el lado, es un cambio radical y es una distracción del trabajo más importante. El equipo de diseño en algún momento hará una recomendación de una forma u otra, estoy seguro. –

Cuestiones relacionadas