web-dev-qa-db-fra.com

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

Ceci est une extension de la question de 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 va si la liste est parcourue plus d'une fois?

101
faulty

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 spécifiquement dans le cas de foreach, vous n'avez pas besoin de faire ceci plus: 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 votre 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 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

159
Marc Gravell