Variable capturada en un bucle en C#
Encontré un problema interesante sobre C#. Tengo un código como el siguiente.
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 genere 0, 2, 4, 6, 8. Sin embargo, en realidad genera cinco decenas.
Parece que se debe a que todas las acciones se refieren a una variable capturada. Como resultado, cuando se invocan, todos tienen el mismo resultado.
¿Hay alguna manera de solucionar este límite para que cada instancia de acción tenga su propia variable capturada?
Sí, tome una copia de la variable dentro del bucle:
while (variable < 5)
{
int copy = variable;
actions.Add(() => copy * 2);
++ variable;
}
Puede pensar en ello como si el compilador de C# creara una variable local "nueva" cada vez que llega a la declaración de variable. De hecho, creará nuevos objetos de cierre apropiados y se complica (en términos de implementación) si hace referencia a variables en múltiples ámbitos, pero funciona :)
Tenga en cuenta que una ocurrencia más común de este problema es el uso 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 también tiene más ejemplos.
Tenga en cuenta que a partir del compilador de C# 5 y posteriores (incluso cuando se especifica una versión anterior de C#), el comportamiento cambió, foreach
por lo que ya no es necesario realizar una copia local. Consulte esta respuesta para obtener más detalles.
Creo que lo que estás experimentando es algo conocido como Cierre http://en.wikipedia.org/wiki/Closure_(computer_science) . Su lamba tiene una referencia a una variable cuyo alcance está fuera de la función misma. Su lamba no se interpreta hasta que lo invoca y una vez que lo haga, obtendrá el valor que tiene la variable en el momento de la ejecución.
Detrás de escena, el compilador genera una clase que representa el cierre de la llamada a su método. Utiliza esa única instancia de la clase de cierre para cada iteración del bucle. El código se parece a este, lo que facilita ver por qué ocurre el error:
void Main()
{
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
var closure = new CompilerGeneratedClosure();
Func<int> anonymousMethodAction = null;
while (closure.variable < 5)
{
if(anonymousMethodAction == null)
anonymousMethodAction = new Func<int>(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;
}
}
En realidad, este no es 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.
Esto no tiene nada que ver con los bucles.
Este comportamiento se activa porque utiliza una expresión lambda () => variable * 2
donde el ámbito externo variable
en realidad no está definido en el alcance 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 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 en variable
su código en realidad se traduce a un cambio en eseClosureClass.variable
Entonces, su bucle while sigue actualizándose ClosureClass.variable
hasta que llega a 10, luego los bucles for ejecutan las acciones, que operan todas de la misma manera ClosureClass.variable
.
Para obtener el resultado esperado, debe crear una separación entre la variable del bucle y la variable que se está cerrando. Puedes hacer esto introduciendo otra variable, es decir:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
var t = variable; // now t will be closured (i.e. 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 puedes mover el cierre a otro método para crear esta separación:
List<Func<int>> actions = new List<Func<int>>();
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<int> 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<int> 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 el uso de métodos anónimos/expresiones lambda de variables de ámbito local, aunque algunos usos imprudentes de los bucles demuestran trampas de cierres.