Variable capturada en un bucle en C #

Me encontré con un problema interesante sobre C #. Tengo un código como a continuación.

List<Func> actions = new List<Func>(); int variable = 0; while (variable  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 la misma salida.

¿Hay alguna manera de trabajar alrededor de este límite para que cada instancia de acción tenga su propia variable capturada?

Sí, tome una copia de la variable dentro del ciclo:

 while (variable < 5) { int copy = variable; actions.Add(() => copy * 2); ++ variable; } 

Puede pensar que el comstackdor de C # crea una variable local “nueva” cada vez que golpea la statement de variable. De hecho, creará nuevos objetos de cierre apropiados, y se complica (en términos de implementación) si se refiere a variables en múltiples ámbitos, pero funciona 🙂

Tenga en cuenta que una ocurrencia más común de este problema es usar for o foreach :

 for (int i=0; i < 10; i++) // Just one variable foreach (string x in foo) // And again, despite how it reads out loud 

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 tiene más ejemplos también.

Creo que lo que estás experimentando es algo conocido como Closure http://en.wikipedia.org/wiki/Closure_(computer_science) . Su lamba tiene una referencia a una variable que tiene un scope fuera de la función en sí. Tu lamba no se interpreta hasta que lo invoques y, una vez que lo consiga, obtendrá el valor que tiene la variable en el momento de la ejecución.

Detrás de escena, el comstackdor está generando una clase que representa el cierre de su llamada de método. Utiliza esa única instancia de la clase de cierre para cada iteración del ciclo. El código se ve más o menos así, lo que hace que sea más fácil ver por qué ocurre el error:

 void Main() { List> actions = new List>(); int variable = 0; var closure = new CompilerGeneratedClosure(); Func anonymousMethodAction = null; while (closure.variable < 5) { if(anonymousMethodAction == null) anonymousMethodAction = new Func(closure.YourAnonymousMethod); //we're re-adding the same function actions.Add(anonymousMethodAction); ++closure.variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } } class CompilerGeneratedClosure { public int variable; public int YourAnonymousMethod() { return this.variable * 2; } } 

Este no es realmente el código comstackdo de su muestra, pero he examinado mi propio código y esto se parece mucho a lo que el comstackdor realmente generaría.

La forma de evitar esto es almacenar el valor que necesita en una variable proxy, y hacer que esa variable sea capturada.

ES DECIR

 while( variable < 5 ) { int copy = variable; actions.Add( () => copy * 2 ); ++variable; } 

Sí, necesita una variable ámbito dentro del ciclo y pasarlo a la lambda de esa manera:

 List> actions = new List>(); int variable = 0; while (variable < 5) { int variable1 = variable; actions.Add(() => variable1 * 2); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } Console.ReadLine(); 

La misma situación está sucediendo en multi-threading (C #, .NET 4.0).

Vea el siguiente código:

El propósito es imprimir 1,2,3,4,5 en orden.

 for (int counter = 1; counter <= 5; counter++) { new Thread (() => Console.Write (counter)).Start(); } 

¡La salida es interesante! (Podría ser como 21334 …)

La única solución es usar variables locales.

 for (int counter = 1; counter <= 5; counter++) { int localVar= counter; new Thread (() => Console.Write (localVar)).Start(); } 

Esto no tiene nada que ver con los bucles.

Este comportamiento se desencadena porque utiliza una expresión lambda () => variable * 2 donde la variable ámbito externo no está realmente definida en el ámbito interno de la 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 implica algunos dilemas (pasar por valor? Pasar por referencia? C # va por 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 de ayuda (“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 en la variable en su código se traduce realmente para cambiar en esa ClosureClass.variable

Entonces, while loop continúa actualizando ClosureClass.variable hasta que llegue a 10, y luego for loops ejecuta las acciones, que operan todas en la misma ClosureClass.variable .

Para obtener el resultado esperado, debe crear una separación entre la variable de ciclo y la variable que se está cerrando. Puedes hacer esto introduciendo otra variable, es decir:

 List> actions = new List>(); int variable = 0; while (variable < 5) { var t = variable; // now t will be closured (ie replaced by a field in the new class) actions.Add(() => t * 2); ++variable; // changing variable won't affect the closured variable t } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

También puede mover el cierre a otro método para crear esta separación:

 List> actions = new List>(); int variable = 0; while (variable < 5) { actions.Add(Mult(variable)); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Puede implementar Mult como una expresión lambda (cierre implícito)

 static Func Mult(int i) { return () => i * 2; } 

o con una clase de ayuda real:

 public class Helper { public int _i; public Helper(int i) { _i = i; } public int Method() { return _i * 2; } } static Func Mult(int i) { Helper help = new Helper(i); return help.Method; } 

En cualquier caso, los "cierres" NO son un concepto relacionado con los bucles , sino más bien con métodos anónimos / expresiones lambda, el uso de variables de ámbito local, aunque un uso incauto de bucles demuestra trampas de cierres.