Encontré un problema interesante sobre C #. Tengo un código como el de abajo.
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => variable * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Espero que produzca 0, 2, 4, 6, 8. Sin embargo, en realidad produce cinco 10s.
Parece que se debe a todas las acciones que se refieren a una variable capturada. Como resultado, cuando se invocan, todos tienen el mismo resultado.
¿Hay alguna forma de evitar este límite para que cada instancia de acción tenga su propia variable capturada?
c#
closures
captured-variable
Morgan Cheng
fuente
fuente
Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
.Respuestas:
Sí, tome una copia de la variable dentro del bucle:
Puede pensar que el compilador de C # crea una variable local "nueva" cada vez que llega a la declaración de variable. De hecho, creará nuevos objetos de cierre apropiados y se complicará (en términos de implementación) si hace referencia a variables en varios ámbitos, pero funciona :)
Tenga en cuenta que una ocurrencia más común de este problema es usar
for
oforeach
:Consulte la sección 7.14.4.2 de la especificación C # 3.0 para obtener más detalles al respecto, y mi artículo sobre cierres también tiene más ejemplos.
Tenga en cuenta que a partir del compilador de C # 5 y más allá (incluso cuando se especifica una versión anterior de C #), el comportamiento de ha
foreach
cambiado, por lo que ya no necesita hacer una copia local. Vea esta respuesta para más detalles.fuente
Creo que lo que está experimentando es algo conocido como Cierre http://en.wikipedia.org/wiki/Closure_(computer_science) . Su lamba tiene una referencia a una variable que se encuentra fuera de la función misma. Su lamba no se interpreta hasta que la invoque y una vez que lo haga, obtendrá el valor que tiene la variable en el momento de la ejecución.
fuente
Detrás de escena, el compilador está generando una clase que representa el cierre de su llamada al método. Utiliza esa única instancia de la clase de cierre para cada iteración del bucle. El código se parece a esto, lo que facilita ver por qué ocurre el error:
Este no es realmente el código compilado de su muestra, pero he examinado mi propio código y se parece mucho a lo que realmente generaría el compilador.
fuente
La forma de evitar esto es almacenar el valor que necesita en una variable proxy y hacer que esa variable sea capturada.
ES DECIR
fuente
Esto no tiene nada que ver con los bucles.
Este comportamiento se desencadena porque utiliza una expresión lambda
() => variable * 2
donde el ámbito externovariable
no está realmente definido en el ámbito interno del lambda.Las expresiones lambda (en C # 3 +, así como los métodos anónimos en C # 2) aún crean métodos reales. Pasar variables a estos métodos involucra algunos dilemas (¿pasar por valor? ¿Pasar por referencia? C # va con referencia - pero esto abre otro problema donde la referencia puede sobrevivir a la variable real). Lo que C # hace para resolver todos estos dilemas es crear una nueva clase auxiliar ("cierre") con campos correspondientes a las variables locales utilizadas en las expresiones lambda y métodos correspondientes a los métodos lambda reales. Cualquier cambio
variable
en su código se traduce realmente como cambio en eseClosureClass.variable
Por lo tanto, su bucle while continúa actualizando
ClosureClass.variable
hasta que alcanza 10, luego usted para bucles ejecuta las acciones, que operan en el mismoClosureClass.variable
.Para obtener el resultado esperado, debe crear una separación entre la variable de bucle y la variable que se está cerrando. Puede hacer esto introduciendo otra variable, es decir:
También puede mover el cierre a otro método para crear esta separación:
Puede implementar Mult como una expresión lambda (cierre implícito)
o con una clase de ayuda real:
En cualquier caso, los "cierres" NO son un concepto relacionado con los bucles , sino más bien con el uso de métodos anónimos / expresiones lambda de variables de ámbito local, aunque algunos usos incautos de bucles demuestran trampas de cierres.
fuente
Sí, debe buscar
variable
dentro del bucle y pasarlo a la lambda de esa manera:fuente
La misma situación está ocurriendo en subprocesos múltiples (C #, .NET 4.0).
Ver el siguiente código:
El propósito es imprimir 1,2,3,4,5 en orden.
¡La salida es interesante! (Puede ser como 21334 ...)
La única solución es usar variables locales.
fuente
Como aquí nadie citó directamente a ECMA-334 :
Más adelante en la especificación,
Ah sí, supongo que debería mencionarse que en C ++ este problema no ocurre porque puedes elegir si la variable se captura por valor o por referencia (ver: captura de Lambda ).
fuente
Se llama el problema de cierre, simplemente use una variable de copia y ya está.
fuente