J'ai rencontré un problème intéressant à propos de C #. J'ai du 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 m'attends à ce qu'il produise 0, 2, 4, 6, 8. Cependant, il produit en fait cinq 10.
Il semble que cela soit dû à toutes les actions se référant à une variable capturée. Par conséquent, lorsqu'ils sont invoqués, ils ont tous la même sortie.
Existe-t-il un moyen de contourner cette limite pour que chaque instance d'action ait sa propre variable capturée?
c#
closures
captured-variable
Morgan Cheng
la source
la source
Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
.Réponses:
Oui - prenez une copie de la variable à l'intérieur de la boucle:
Vous pouvez y penser comme si le compilateur C # crée une "nouvelle" variable locale chaque fois qu'il frappe la déclaration de variable. En fait, cela créera de nouveaux objets de fermeture appropriés, et cela se compliquera (en termes d'implémentation) si vous faites référence à des variables dans plusieurs étendues, mais cela fonctionne :)
Notez qu'une occurrence plus courante de ce problème utilise
for
ouforeach
: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.
Notez que depuis le compilateur C # 5 et au-delà (même lorsque vous spécifiez une version antérieure de C #), le comportement de a
foreach
changé de sorte que vous n'avez plus besoin de faire de copie locale. Voir cette réponse pour plus de détails.la source
Je crois que ce que vous vivez est quelque chose connu sous le nom de fermeture http://en.wikipedia.org/wiki/Closure_(computer_science) . Votre lamba a une référence à une variable qui est portée 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 l'est, il obtiendra la valeur de la variable au moment de l'exécution.
la source
Dans les coulisses, le compilateur génère une classe qui représente la fermeture de votre appel de méthode. Il utilise cette seule instance 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:
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 réellement.
la source
La solution consiste à stocker la valeur dont vous avez besoin dans une variable proxy et à faire capturer cette variable.
C'EST À DIRE
la source
Cela n'a rien à voir avec les boucles.
Ce comportement est déclenché car vous utilisez une expression lambda
() => variable * 2
où la portée externevariable
n'est pas réellement 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. Passer des variables à ces méthodes implique certains dilemmes (passer par valeur? Passer par référence? C # va de pair avec référence - mais cela ouvre un autre problème où la référence peut survivre à la variable réelle). Ce que C # fait pour résoudre tous ces dilemmes est de créer 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
votre code sont en fait traduites pour en changerClosureClass.variable
Ainsi, votre boucle while met à jour la
ClosureClass.variable
jusqu'à ce qu'elle atteigne 10, puis vous pour les boucles exécute les actions, qui fonctionnent toutes sur la mêmeClosureClass.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:
Vous pouvez également déplacer la fermeture vers une autre méthode pour créer cette séparation:
Vous pouvez implémenter Mult comme une expression lambda (fermeture implicite)
ou avec une classe d'assistance réelle:
Dans tous les cas, les «fermetures» ne sont PAS un concept lié aux boucles , mais plutôt à l'utilisation de méthodes / expressions lambda anonymes de variables de portée locale - bien que certaines utilisations imprudentes des boucles démontrent des pièges à fermetures.
la source
Oui, vous devez définir la portée
variable
dans la boucle et la transmettre au lambda de cette façon:la source
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.
La sortie est intéressante! (Cela pourrait être comme 21334 ...)
La seule solution est d'utiliser des variables locales.
la source
Étant donné que personne ici n'a cité directement ECMA-334 :
Plus loin dans la spécification,
Oh oui, je suppose qu'il convient de mentionner qu'en C ++, ce problème ne se produit pas car vous pouvez choisir si la variable est capturée par valeur ou par référence (voir: Capture Lambda ).
la source
Cela s'appelle le problème de fermeture, utilisez simplement une variable de copie, et c'est fait.
la source