Accès à la fermeture modifiée (2)

101

Ceci est une extension de la question de l' accès à la fermeture modifiée . Je veux juste vérifier si ce qui suit est réellement suffisamment sûr pour une utilisation en production.

List<string> lists = new List<string>();
//Code to retrieve lists from DB    
foreach (string list in lists)
{
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(list); });
}

Je n'exécute ce qui précède qu'une fois par démarrage. Pour l'instant, cela semble bien fonctionner. Comme Jon l'a mentionné à propos du résultat contre-intuitif dans certains cas. Alors, que dois-je faire attention ici? Est-ce que ça ira si la liste est parcourue plus d'une fois?

défectueux
la source
18
Félicitations, vous faites maintenant partie de la documentation Resharper. confluence.jetbrains.net/display/ReSharper/…
Kongress
1
Celui-ci était délicat mais l'explication ci-dessus m'a clairement montré: cela peut sembler correct mais, en fait, seule la dernière valeur de la variable str sera utilisée chaque fois qu'un bouton sera cliqué. La raison en est que foreach se déroule dans une boucle while, mais la variable d'itération est définie en dehors de cette boucle. Cela signifie qu'au moment où vous affichez la boîte de message, la valeur de str peut avoir déjà été itérée à la dernière valeur de la collection de chaînes.
DanielV

Réponses:

159

Avant C # 5, vous devez re-déclarer une variable à l' intérieur du foreach - sinon, elle est partagée et tous vos gestionnaires utiliseront la dernière chaîne:

foreach (string list in lists)
{
    string tmp = list;
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(tmp); });
}

De manière significative, notez qu'à partir de C # 5, cela a changé, et en particulier dans le cas deforeach , vous n'avez plus besoin de le faire: le code de la question fonctionnerait comme prévu.

Pour montrer que cela ne fonctionne pas sans cette modification, tenez compte des éléments suivants:

string[] names = { "Fred", "Barney", "Betty", "Wilma" };
using (Form form = new Form())
{
    foreach (string name in names)
    {
        Button btn = new Button();
        btn.Text = name;
        btn.Click += delegate
        {
            MessageBox.Show(form, name);
        };
        btn.Dock = DockStyle.Top;
        form.Controls.Add(btn);
    }
    Application.Run(form);
}

Exécutez ce qui précède avant C # 5 , et bien que chaque bouton affiche un nom différent, cliquez sur les boutons pour afficher «Wilma» quatre fois.

En effet, la spécification du langage (ECMA 334 v4, 15.8.4) (avant C # 5) définit:

foreach (V v in x) embedded-statement est ensuite étendu à:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
         while (e.MoveNext()) {
            v = (V)(T)e.Current;
             embedded-statement
        }
    }
    finally {
         // Dispose e
    }
}

Notez que la variable v(qui est la vôtre list) est déclarée en dehors de la boucle. Ainsi, selon les règles des variables capturées, toutes les itérations de la liste partageront le détenteur de la variable capturée.

À partir de C # 5, cela est changé: la variable d'itération ( v) est portée à l' intérieur de la boucle. Je n'ai pas de référence de spécification, mais cela devient essentiellement:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        while (e.MoveNext()) {
            V v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
         // Dispose e
    }
}

Re désabonnement; si vous souhaitez activement désinscrire un gestionnaire anonyme, l'astuce consiste à capturer le gestionnaire lui-même:

EventHandler foo = delegate {...code...};
obj.SomeEvent += foo;
...
obj.SomeEvent -= foo;

De même, si vous voulez un gestionnaire d'événements unique (tel que Load, etc.):

EventHandler bar = null; // necessary for "definite assignment"
bar = delegate {
  // ... code
  obj.SomeEvent -= bar;
};
obj.SomeEvent += bar;

Ceci est maintenant auto-désabonnement ;-p

Marc Gravell
la source
Si tel est le cas, la variable temporaire restera en mémoire jusqu'à la fermeture de l'application, comme pour servir le délégué, et il ne sera pas conseillé de le faire pour de très grosses boucles si la variable prend beaucoup de mémoire. Ai-je raison?
défectueux
1
Il restera en mémoire pendant la durée pendant laquelle il y a des choses (boutons) avec l'événement. Il existe un moyen de désinscrire des délégués uniques, que j'ajouterai au message.
Marc Gravell
2
Mais pour nuancer sur votre point: oui, les variables capturées peuvent en effet augmenter la portée d'une variable. Vous devez faire attention à ne pas capturer des choses auxquelles vous ne vous attendiez pas ...
Marc Gravell
1
Pourriez-vous s'il vous plaît mettre à jour votre réponse en ce qui concerne la modification de la spécification C # 5.0? Juste pour en faire une excellente documentation wiki concernant les boucles foreach en C #. Il y a déjà de bonnes réponses concernant le changement dans le compilateur C # 5.0 traitant les boucles foreach bit.ly/WzBV3L , mais ce ne sont pas des ressources de type wiki.
Ilya Ivanov
1
@Kos oui, forinchangé dans 5.0
Marc Gravell