2010-02-09 11 views
20

Je viens d'un contexte de programmation fonctionnelle en ce moment, alors pardonnez-moi si je ne comprends pas les fermetures en C#.Fermetures dans les délégués du gestionnaire d'événements C#?

J'ai le code suivant pour générer dynamiquement des boutons qui obtiennent des gestionnaires d'événements anonymes:

for (int i = 0; i < 7; i++) 
{ 
    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + i); 
    }; 

    this.Controls.Add(newButton); 
} 

attendu Le texte "I am button number " + i être fermé à la valeur de i à cette itération de la boucle. Cependant, quand je cours réellement le programme, chaque bouton indique I am button number 7. Qu'est-ce que je rate? J'utilise VS2005. Edit: Donc, je suppose que ma prochaine question est, comment puis-je capturer la valeur?

+4

Vous ne capturez pas la valeur. Vous ne capturez jamais de valeurs, seulement des variables. Pour plus d'informations sur ce problème, voir http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx et http: //blogs.msdn .com/ericlippert/archive/2009/11/16/fermeture-sur-la-boucle-variable-partie-deux.aspx –

Répondre

26

Pour obtenir ce comportement, vous devez copier la variable locale, ne pas utiliser l'itérateur:

for (int i = 0; i < 7; i++) 
{ 
    var inneri = i; 
    Button newButton = new Button(); 
    newButton.Text = "Click me!"; 
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + inneri); 
    }; 
    this.Controls.Add(newButton); 
} 

Le raisonnement est discuté beaucoup plus en détail in this question.

4

La fermeture capture la variable et non la valeur. Cela signifie qu'au moment où le délégué est exécuté, c'est-à-dire après la fin de la boucle, la valeur de i est 6.

Pour capturer une valeur, affectez-la à une variable déclarée dans le corps de la boucle. À chaque itération de la boucle, une nouvelle instance sera créée pour chaque variable déclarée à l'intérieur de celle-ci.

Jon Skeet's articles on closures a une explication plus profonde et plus d'exemples.

for (int i = 0; i < 7; i++) 
{ 
    var copy = i; 

    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + copy); 
    }; 

    this.Controls.Add(newButton); 
} 
+0

-1: une réponse plus informative qui explique, en profondeur, ce qui se passe avec un exemple devrait être évalué plus haut. – IAbstract

1

Au moment où vous cliquez sur un bouton, ils ont tous été générés 1 à 7, afin qu'ils expriment tous l'état final de i qui est 7.

4

Vous avez créé sept délégués, mais chaque délégué détient une référence à la même instance de i.

La fonction MessageBox.Show est appelée uniquement lorsque le bouton est cliqué sur. Au moment où le bouton a cliqué, la boucle est terminée. Donc, à ce stade i sera égal à sept.

Essayez ceci:

for (int i = 0; i < 7; i++) 
{ 

    Button newButton = new Button(); 

    newButton.Text = "Click me!"; 

    int iCopy = i; // There will be a new instance of this created each iteration 
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show("I am button number " + iCopy); 
    }; 

    this.Controls.Add(newButton); 
} 
23

Nick a raison, mais je voulais expliquer un peu mieux dans le texte de cette question exactement pourquoi .

Le problème n'est pas la fermeture; c'est la boucle for. La boucle crée seulement une variable "i" pour la boucle entière. Il ne crée pas une nouvelle variable "i" pour chaque itération. Note: Cela aurait changé pour C# 5.

Cela signifie que lorsque votre délégué anonyme capture ou ferme sur cette variable « i » il est fermeture sur une variable qui est partagée par tous les boutons. Au moment où vous cliquez sur l'un de ces boutons, la boucle a déjà fini d'incrémenter cette variable jusqu'à 7.

La seule chose que je pourrais faire différemment à partir du code de Nick est d'utiliser une chaîne pour la variable intérieure et construire toutes les chaînes à l'avant plutôt que au moment bouton presse, comme ceci:

for (int i = 0; i < 7; i++) 
{ 
    var message = string.Format("I am button number {0}.", i); 

    Button newButton = new Button(); 
    newButton.Text = "Click me!"; 
    newButton.Click += delegate(Object sender, EventArgs e) 
    { 
     MessageBox.Show(message); 
    }; 
    this.Controls.Add(newButton); 
} 

qui négocie juste un petit peu de mémoire (en conservant des variables de chaînes plus grandes au lieu d'entiers) pour un peu de temps CPU plus tard ... cela dépend de votre application ce qui compte le plus.

Une autre option est de ne pas coder manuellement la boucle du tout:

this.Controls.AddRange(Enumerable.Range(0,7).Select(i => 
{ 
    var b = new Button() {Text = "Click me!", Top = i * 20}; 
    b.Click += (s,e) => MessageBox.Show(string.Format("I am button number {0}.", i)); 
    return b; 
}).ToArray()); 

J'aime cette dernière option non pas tant parce qu'elle supprime la boucle, mais parce qu'il vous commence à penser en termes de construction de ce contrôle à partir d'un la source de données.

+0

+1 pour une amélioration supplémentaire! –

+0

Ce n'est pas un bug mais ils le modifient. Tout comme Silverlight est un framework viable (mais pourrait ne jamais avoir de nouvelles fonctionnalités et a réduit/supprimé le support). – micahhoover