Erfasste Variable in einer Schleife in C #

Ich habe ein interessantes Thema über C # getroffen. Ich habe Code wie unten.

List<Func> actions = new List<Func>(); int variable = 0; while (variable  variable * 2); ++ variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

Ich erwarte, dass es 0, 2, 4, 6, 8 ausgibt. Es gibt jedoch tatsächlich fünf 10s aus.

Es scheint, dass dies auf alle Aktionen zurückzuführen ist, die sich auf eine erfasste Variable beziehen. Wenn sie aufgerufen werden, haben sie alle dieselbe Ausgabe.

Gibt es eine Möglichkeit, dieses Limit zu umgehen, damit jede Action-Instanz ihre eigene erfasste Variable hat?

   

    Ja – nehmen Sie eine Kopie der Variablen innerhalb der Schleife:

     while (variable < 5) { int copy = variable; actions.Add(() => copy * 2); ++ variable; } 

    Sie können sich das so vorstellen, als ob der C # -Compiler jedes Mal, wenn er die Variablendeklaration trifft, eine “neue” lokale Variable erzeugt. In der Tat wird es geeignete neue Closure-Objekte erstellen, und es wird kompliziert (in Bezug auf die Implementierung), wenn Sie auf Variablen in mehreren Bereichen verweisen, aber es funktioniert 🙂

    Beachten Sie, dass ein häufigeres Auftreten dieses Problems for oder foreach :

     for (int i=0; i < 10; i++) // Just one variable foreach (string x in foo) // And again, despite how it reads out loud 

    Weitere Einzelheiten hierzu finden Sie in Abschnitt 7.14.4.2 der Spezifikation C # 3.0, und in meinem Artikel zu Schließungen finden Sie weitere Beispiele.

    Ich glaube, was Sie erleben, ist etwas, das als Closure http://en.wikipedia.org/wiki/Closure_(computer_science) bekannt ist . Ihre Lamba hat eine Referenz auf eine Variable, die außerhalb der function selbst liegt. Ihr lamba wird nicht interpretiert, bis Sie es aufrufen, und wenn es einmal ist, wird es den Wert erhalten, den die Variable zur Ausführungszeit hat.

    Hinter den Kulissen generiert der Compiler eine class, die den Abschluss für Ihren Methodenaufruf darstellt. Es verwendet diese einzelne Instanz der Closure-class für jede Iteration der Schleife. Der Code sieht in etwa so aus, was es einfacher macht zu erkennen, warum der Fehler auftritt:

     void Main() { List> actions = new List>(); int variable = 0; var closure = new CompilerGeneratedClosure(); Func anonymousMethodAction = null; while (closure.variable < 5) { if(anonymousMethodAction == null) anonymousMethodAction = new Func(closure.YourAnonymousMethod); //we're re-adding the same function actions.Add(anonymousMethodAction); ++closure.variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } } class CompilerGeneratedClosure { public int variable; public int YourAnonymousMethod() { return this.variable * 2; } } 

    Dies ist nicht wirklich der kompilierte Code aus Ihrem Beispiel, aber ich habe meinen eigenen Code untersucht und das sieht sehr ähnlich aus, was der Compiler tatsächlich erzeugen würde.

    Um dies zu vermeiden, speichern Sie den benötigten Wert in einer Proxy-Variablen und lassen Sie diese Variable erfassen.

    IE

     while( variable < 5 ) { int copy = variable; actions.Add( () => copy * 2 ); ++variable; } 

    Ja, Sie müssen die variable innerhalb der Schleife skalieren und sie so an das Lambda übergeben:

     List> actions = new List>(); int variable = 0; while (variable < 5) { int variable1 = variable; actions.Add(() => variable1 * 2); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } Console.ReadLine(); 

    Die gleiche Situation tritt bei Multi-Threading auf (C #, .NET 4.0).

    Siehe den folgenden Code:

    Zweck ist 1,2,3,4,5 in Reihenfolge zu drucken.

     for (int counter = 1; counter < = 5; counter++) { new Thread (() => Console.Write (counter)).Start(); } 

    Die Ausgabe ist interessant! (Es könnte wie 21334 sein …)

    Die einzige Lösung ist die Verwendung lokaler Variablen.

     for (int counter = 1; counter < = 5; counter++) { int localVar= counter; new Thread (() => Console.Write (localVar)).Start(); } 

    Das hat nichts mit Schleifen zu tun.

    Dieses Verhalten wird ausgetriggers, weil Sie einen Lambda-Ausdruck () => variable * 2 wobei die äußere Bereichsvariable nicht wirklich im inneren Bereich des Lambda definiert ist.

    Lambda-Ausdrücke (in C # 3 + sowie anonyme Methoden in C # 2) erzeugen immer noch tatsächliche Methoden. Das Übergeben von Variablen an diese Methoden beinhaltet einige Dilemmata (pass by value? Pass by reference? C # geht mit Verweis – aber dies öffnet ein anderes Problem, wo die Referenz die tatsächliche Variable überleben kann). Was C # tut, um alle diese Dilemmas aufzulösen, besteht darin, eine neue Hilfsklasse (“closure”) mit Feldern zu erzeugen, die den lokalen Variablen in den Lambda-Ausdrücken entsprechen, und Methoden, die den tatsächlichen Lambda-Methoden entsprechen. Alle Änderungen an der variable in Ihrem Code werden tatsächlich in die ClosureClass.variable

    ClosureClass.variable Ihre while-Schleife die ClosureClass.variable aktualisiert, bis sie 10 erreicht, führt Sie for loops die Aktionen aus, die alle auf derselben ClosureClass.variable .

    Um das erwartete Ergebnis zu erhalten, müssen Sie eine Trennung zwischen der Schleifenvariablen und der zu schließenden Variablen erstellen. Sie können dies tun, indem Sie eine andere Variable einführen, zB:

     List> actions = new List>(); int variable = 0; while (variable < 5) { var t = variable; // now t will be closured (ie replaced by a field in the new class) actions.Add(() => t * 2); ++variable; // changing variable won't affect the closured variable t } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

    Sie können den Abschluss auch auf eine andere Methode verschieben, um diese Trennung zu erstellen:

     List> actions = new List>(); int variable = 0; while (variable < 5) { actions.Add(Mult(variable)); ++variable; } foreach (var act in actions) { Console.WriteLine(act.Invoke()); } 

    Sie können Mult als Lambda-Ausdruck implementieren (implizite Schließung)

     static Func Mult(int i) { return () => i * 2; } 

    oder mit einer tatsächlichen Helferklasse:

     public class Helper { public int _i; public Helper(int i) { _i = i; } public int Method() { return _i * 2; } } static Func Mult(int i) { Helper help = new Helper(i); return help.Method; } 

    In jedem Fall sind "Closures" KEINE Konzepte, die sich auf Schleifen beziehen , sondern eher auf anonyme Methoden / Lambda-Ausdrücke, bei denen lokale Bereichsvariablen verwendet werden - obwohl unvorsichtiges Verwenden von Schleifen Verschlüsse-Traps zeigt.