Zugang zu modifizierter Schließung (2)

101

Dies ist eine Erweiterung der Frage von Access to Modified Closure . Ich möchte nur überprüfen, ob das Folgende tatsächlich sicher genug für die Verwendung in der Produktion ist.

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); });
}

Ich laufe die oben genannten nur einmal pro Start durch. Im Moment scheint es in Ordnung zu funktionieren. Wie Jon über kontraintuitives Ergebnis in einigen Fällen erwähnt hat. Was muss ich hier beachten? Wird es in Ordnung sein, wenn die Liste mehr als einmal durchlaufen wird?

fehlerhaft
quelle
18
Herzlichen Glückwunsch, Sie sind jetzt Teil der Resharper-Dokumentation. konfluenz.jetbrains.net/display/ReSharper/…
Kongress
1
Dieser war schwierig, aber die obige Erklärung machte mir klar: Dies mag richtig erscheinen, aber tatsächlich wird nur der letzte Wert der Variablen str verwendet, wenn auf eine Schaltfläche geklickt wird. Der Grund dafür ist, dass foreach in einer while-Schleife abgewickelt wird, die Iterationsvariable jedoch außerhalb dieser Schleife definiert ist. Dies bedeutet, dass zum Zeitpunkt der Anzeige des Meldungsfelds der Wert von str möglicherweise bereits bis zum letzten Wert in der Zeichenfolgensammlung iteriert wurde.
DanielV

Antworten:

159

Vor C # 5 müssen Sie eine Variable in foreach erneut deklarieren. Andernfalls wird sie gemeinsam genutzt, und alle Ihre Handler verwenden die letzte Zeichenfolge:

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

Beachten Sie, dass sich dies ab C # 5 geändert hat und Sie dies insbesondere im Fall vonforeach nicht mehr tun müssen: Der Code in der Frage würde wie erwartet funktionieren.

Beachten Sie Folgendes, um zu zeigen, dass dies ohne diese Änderung nicht funktioniert:

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);
}

Führen Sie die obigen Schritte vor C # 5 aus . Obwohl jede Schaltfläche einen anderen Namen aufweist, wird beim Klicken auf die Schaltflächen viermal "Wilma" angezeigt.

Dies liegt daran, dass die Sprachspezifikation (ECMA 334 v4, 15.8.4) (vor C # 5) definiert:

foreach (V v in x) embedded-statement wird dann erweitert auf:

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

Beachten Sie, dass die Variable v(die Ihre ist list) außerhalb der Schleife deklariert ist . Nach den Regeln der erfassten Variablen teilen sich alle Iterationen der Liste den Inhaber der erfassten Variablen.

Ab C # 5 wird dies geändert: Die Iterationsvariable ( v) befindet sich innerhalb der Schleife. Ich habe keine Spezifikationsreferenz, aber es wird im Grunde:

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

Abmelden; Wenn Sie einen anonymen Handler aktiv abbestellen möchten, besteht der Trick darin, den Handler selbst zu erfassen:

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

Ebenso, wenn Sie einen einmaligen Ereignishandler (wie z. B. Laden usw.) möchten:

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

Dies ist jetzt selbst abbestellend ;-p

Marc Gravell
quelle
In diesem Fall bleibt die temporäre Variable im Speicher, bis die App geschlossen wird, um dem Delegaten zu dienen, und es ist nicht ratsam, dies für sehr große Schleifen zu tun, wenn die Variable viel Speicher belegt. Habe ich recht?
fehlerhafter
1
Es bleibt so lange im Speicher, wie es Dinge (Schaltflächen) mit dem Ereignis gibt. Es gibt eine Möglichkeit, einmalige Delegierte abzumelden, die ich dem Beitrag hinzufügen werde.
Marc Gravell
2
Aber um sich in Ihrem Punkt zu qualifizieren: Ja, erfasste Variablen können tatsächlich den Umfang einer Variablen erhöhen. Sie müssen aufpassen, dass Sie keine Dinge erfassen, die Sie nicht erwartet haben ...
Marc Gravell
1
Könnten Sie bitte Ihre Antwort in Bezug auf Änderungen in der C # 5.0-Spezifikation aktualisieren? Nur um es zu einer großartigen Wiki-Dokumentation zu foreach-Schleifen in C # zu machen. Es gibt bereits einige gute Antworten bezüglich der Änderung des C # 5.0-Compilers, der foreach-Schleifen bit.ly/WzBV3L behandelt , aber es handelt sich nicht um wikiähnliche Ressourcen.
Ilya Ivanov
1
@ Kos ja, forist unverändert in 5.0
Marc Gravell