J'ai rencontré un problème intéressant à propos de C #. J'ai un code comme ci-dessous.
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());
}
Je pense qu’il produira 0, 2, 4, 6, 8. Cependant, il délivre en réalité cinq 10.
Il semble que cela soit dû à toutes les actions faisant référence à une variable capturée. En conséquence, quand ils sont appelés, ils ont tous le même résultat.
Existe-t-il un moyen de contourner cette limite pour que chaque instance d'action ait sa propre variable capturée?
Oui - prenez une copie de la variable à l'intérieur de la boucle:
while (variable < 5)
{
int copy = variable;
actions.Add(() => copy * 2);
++ variable;
}
Vous pouvez penser à cela comme si le compilateur C # créait une "nouvelle" variable locale chaque fois qu'il atteignait la déclaration de variable. En fait, cela va créer de nouveaux objets de fermeture appropriés, et cela devient compliqué (en termes de mise en œuvre) si vous faites référence à des variables dans plusieurs portées, mais cela fonctionne :)
Notez qu'une occurrence plus courante de ce problème utilise for
ou foreach
:
for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud
Voir la section 7.14.4.2 de la spécification C # 3.0 pour plus de détails à ce sujet, et mon article sur les fermetures contient également d'autres exemples.
Je crois que ce que vous vivez est ce que l’on appelle la fermeture http://en.wikipedia.org/wiki/Closure_ (computer_science) . Votre lamba a une référence à une variable qui est étendue en dehors de la fonction elle-même. Votre lamba n'est pas interprété jusqu'à ce que vous l'invoquiez et une fois qu'il obtiendra la valeur de la variable au moment de l'exécution.
En coulisse, le compilateur génère une classe qui représente la clôture de votre appel de méthode. Il utilise cette instance unique de la classe de fermeture pour chaque itération de la boucle. Le code ressemble à ceci, ce qui permet de voir plus facilement pourquoi le bogue se produit:
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;
}
}
Ce n'est pas réellement le code compilé de votre exemple, mais j'ai examiné mon propre code et cela ressemble beaucoup à ce que le compilateur générerait.
La solution consiste à stocker la valeur dont vous avez besoin dans une variable proxy et à la capturer.
C'EST À DIRE.
while( variable < 5 )
{
int copy = variable;
actions.Add( () => copy * 2 );
++variable;
}
Oui, vous devez inclure variable
dans la boucle et le transmettre au lambda de cette façon:
List<Func<int>> actions = new List<Func<int>>();
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 même situation se produit dans le multi-threading (C #, . NET 4.0].
Voir le code suivant:
Le but est d’imprimer 1,2,3,4,5 dans l’ordre.
for (int counter = 1; counter <= 5; counter++)
{
new Thread (() => Console.Write (counter)).Start();
}
La sortie est intéressante! (Cela pourrait être comme 21334 ...)
La seule solution consiste à utiliser des variables locales.
for (int counter = 1; counter <= 5; counter++)
{
int localVar= counter;
new Thread (() => Console.Write (localVar)).Start();
}
Ce comportement est déclenché car vous utilisez une expression lambda () => variable * 2
où la portée externe variable
n'est pas définie dans la portée interne du lambda.
Les expressions lambda (en C # 3 +, ainsi que les méthodes anonymes en C # 2) créent toujours des méthodes réelles. Le passage de variables à ces méthodes soulève certains dilemmes (passage par valeur? Passage par référence? C # est associé à référence - mais cela pose un autre problème dans lequel la référence peut survivre à la variable réelle). Pour résoudre tous ces dilemmes, C # crée une nouvelle classe d’assistance ("fermeture") avec des champs correspondant aux variables locales utilisées dans les expressions lambda et des méthodes correspondant aux méthodes lambda réelles. Toute modification apportée à variable
dans votre code est traduite pour être modifiée dans cette ClosureClass.variable
Donc, votre boucle while continue de mettre à jour le ClosureClass.variable
jusqu’à ce qu’il atteigne 10, alors vous exécutez les actions pour toutes les boucles, qui fonctionnent toutes sur le même ClosureClass.variable
.
Pour obtenir le résultat attendu, vous devez créer une séparation entre la variable de boucle et la variable en cours de fermeture. Vous pouvez le faire en introduisant une autre variable, à savoir:
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());
}
Vous pouvez également déplacer la fermeture vers une autre méthode pour créer cette séparation:
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());
}
Vous pouvez implémenter Mult en tant qu'expression lambda (fermeture implicite)
static Func<int> Mult(int i)
{
return () => i * 2;
}
ou avec une classe d'assistance réelle:
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;
}
Dans tous les cas, "Les fermetures" NE SONT PAS un concept lié aux boucles, mais plutôt aux méthodes anonymes/expressions lambda qui utilisent des variables de portée locale - bien que certaines utilisations imprudentes de boucles démontrent des pièges de fermeture.
C'est ce qu'on appelle le problème de fermeture, utilisez simplement une variable de copie, et c'est fait.
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
int i = variable;
actions.Add(() => i * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}