Ich habe ein interessantes Problem mit C # getroffen. Ich habe Code wie unten.
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => 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 Aktionsinstanz eine eigene erfasste Variable hat?
c#
closures
captured-variable
Morgan Cheng
quelle
quelle
Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
.Antworten:
Ja - kopieren Sie die Variable in der Schleife:
Sie können sich vorstellen, dass der C # -Compiler jedes Mal, wenn er auf die Variablendeklaration trifft, eine "neue" lokale Variable erstellt. Tatsächlich werden geeignete neue Abschlussobjekte erstellt, und es wird (in Bezug auf die Implementierung) kompliziert, wenn Sie auf Variablen in mehreren Bereichen verweisen, aber es funktioniert :)
Beachten Sie, dass ein häufiges Auftreten dieses Problems wird mit
for
oderforeach
:Weitere Informationen hierzu finden Sie in Abschnitt 7.14.4.2 der C # 3.0-Spezifikation. In meinem Artikel zu Verschlüssen finden Sie auch weitere Beispiele.
Beachten Sie, dass sich ab dem C # 5-Compiler und darüber hinaus (auch wenn Sie eine frühere Version von C # angeben) das Verhalten von
foreach
geändert hat, sodass Sie keine lokale Kopie mehr erstellen müssen. Siehe diese Antwort für weitere Details.quelle
Ich glaube, was Sie erleben, ist etwas, das als Schließung bekannt ist ( http://en.wikipedia.org/wiki/Closure_(computer_science) . Ihre Lamba hat einen Verweis auf eine Variable, die außerhalb der Funktion selbst liegt. Ihre Lamba wird erst interpretiert, wenn Sie sie aufrufen. Sobald sie verfügbar ist, erhält sie den Wert, den die Variable zur Ausführungszeit hat.
quelle
Hinter den Kulissen generiert der Compiler eine Klasse, die den Abschluss Ihres Methodenaufrufs darstellt. Es verwendet diese einzelne Instanz der Abschlussklasse für jede Iteration der Schleife. Der Code sieht ungefähr so aus, wodurch leichter zu erkennen ist, warum der Fehler auftritt:
Dies ist eigentlich nicht der kompilierte Code aus Ihrem Beispiel, aber ich habe meinen eigenen Code untersucht und dies sieht sehr nach dem aus, was der Compiler tatsächlich generieren würde.
quelle
Um dies zu umgehen, müssen Sie den benötigten Wert in einer Proxy-Variablen speichern und diese Variable erfassen lassen.
IE
quelle
Dies hat nichts mit Schleifen zu tun.
Dieses Verhalten wird ausgelöst, weil Sie einen Lambda-Ausdruck verwenden,
() => variable * 2
bei dem der äußere Bereichvariable
nicht tatsächlich im inneren Bereich des Lambda definiert ist.Lambda-Ausdrücke (in C # 3 + sowie anonyme Methoden in C # 2) erstellen weiterhin tatsächliche Methoden. Das Übergeben von Variablen an diese Methoden ist mit einigen Dilemmata verbunden (Übergeben von Wert? Übergeben von Referenz? C # geht mit Referenz einher - dies eröffnet jedoch ein weiteres Problem, bei dem die Referenz die tatsächliche Variable überleben kann). Um all diese Dilemmata zu lösen, erstellt C # eine neue Hilfsklasse ("Closure") mit Feldern, die den in den Lambda-Ausdrücken verwendeten lokalen Variablen entsprechen, und Methoden, die den tatsächlichen Lambda-Methoden entsprechen. Alle Änderungen an
variable
Ihrem Code werden tatsächlich übersetzt, um dies zu ändernClosureClass.variable
Ihre while-Schleife wird also so lange aktualisiert,
ClosureClass.variable
bis sie 10 erreicht. Dann führen Sie for-Schleifen die Aktionen aus, die alle auf derselben Weise ausgeführt werdenClosureClass.variable
.Um das erwartete Ergebnis zu erzielen, müssen Sie eine Trennung zwischen der Schleifenvariablen und der Variablen, die geschlossen wird, erstellen. Sie können dies tun, indem Sie eine andere Variable einführen, dh:
Sie können den Verschluss auch auf eine andere Methode verschieben, um diese Trennung zu erstellen:
Sie können Mult als Lambda-Ausdruck implementieren (impliziter Abschluss).
oder mit einer tatsächlichen Helferklasse:
In jedem Fall handelt es sich bei "Closures" NICHT um ein Konzept, das sich auf Schleifen bezieht , sondern auf anonyme Methoden / Lambda-Ausdrücke, bei denen Variablen mit lokalem Gültigkeitsbereich verwendet werden - obwohl einige vorsichtige Verwendung von Schleifen Schließfallen aufzeigen.
quelle
Ja, Sie müssen
variable
innerhalb der Schleife einen Bereich erstellen und auf diese Weise an das Lambda übergeben:quelle
Die gleiche Situation tritt beim Multithreading auf (C #, .NET 4.0].
Siehe folgenden Code:
Zweck ist es, 1,2,3,4,5 der Reihe nach zu drucken.
Die Ausgabe ist interessant! (Es könnte wie 21334 sein ...)
Die einzige Lösung besteht darin, lokale Variablen zu verwenden.
quelle
Da hier niemand ECMA-334 direkt zitierte :
Weiter in der Spezifikation,
Oh ja, ich denke, es sollte erwähnt werden, dass dieses Problem in C ++ nicht auftritt, da Sie auswählen können, ob die Variable nach Wert oder Referenz erfasst wird (siehe: Lambda-Erfassung ).
quelle
Es wird als Abschlussproblem bezeichnet. Verwenden Sie einfach eine Kopiervariable, und fertig.
quelle