2008-11-07 16 views
154

Ich traf ein interessantes Thema über C#. Ich habe Code wie unten.Captured Variable in einer Schleife in C#

Ich erwarte, dass es 0, 2, 4, 6, 8 ausgibt. Jedoch gibt es 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 eine eigene erfasste Variable hat?

+9

Siehe auch Eric Lipperts Blog-Serie zum Thema: [Schließen der Loop-Variable als schädlich] (http://blogs.msdn.com/b/ericlippert/archive/tags/closures/) – Brian

+6

Auch sie ändern sich C# 5, um so zu arbeiten, wie du es innerhalb einer foreach erwartest. (Breaking Change) –

+0

Related: [Warum-ist-es-schlecht-zu-verwenden-eine-Iteration-Variable-in-einem Lambda-Ausdruck] (http://stackoverflow.com/questions/227820/why-is -it-bad-to-use-iteration-variable-in-a-lambda-expression) – nawfal

Antwort

137

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

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

Sie daran denken können, als ob der C# -Compiler jedes Mal trifft es die Variablendeklaration eine „neue“ lokale Variable erzeugt. In der Tat wird es entsprechende 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 ist 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 

Siehe Abschnitt 7.14.4.2 der C# 3.0-Spezifikation für weitere Einzelheiten über diese und meine article on closures haben auch weitere Beispiele.

+19

Jon's Buch hat auch ein sehr gutes Kapitel darüber (hören Sie demütig auf, Jon!) –

+21

Es sieht besser aus, wenn ich andere Leute einstecken lasse;) (Ich gestehe, dass ich dazu tendiere, Antworten zu wählen, die es jedoch empfehlen.) –

+2

jemals, Feedback zu [email protected] würde geschätzt werden :) –

4

Ja müssen Sie Umfang variable innerhalb der Schleife und geben es auf diese Weise zu dem Lambda:

List<Func<int>> actions = new List<Func<int>>(); 

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

Der Weg, um dieses ist es, den Wert speichern Sie in einem Proxy-Variable benötigen, und haben diese Variable get gefangen.

I.E.

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

Ja, es funktioniert. Aber warum? –

+0

Siehe die Erklärung in meiner bearbeiteten Antwort. Ich finde jetzt das relevante Teil der Spezifikation. –

+0

Haha Jon, ich lese eigentlich nur Ihren Artikel: http://csharpindepth.com/Articles/Chapter5/Closures.aspx Sie machen gute Arbeit mein Freund. – tjlevine

16

Ich glaube, was Sie erleben etwas wie Closure http://en.wikipedia.org/wiki/Closure_(computer_science) bekannt ist. Ihre Lamba hat eine Referenz auf eine Variable, die außerhalb der Funktion 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. .

2

Die gleiche Situation ist in Multi-Threading (C# geschieht, .NET 4.0]

Siehe den folgenden Code:.

Zweck ist 1,2,3,4,5 zu drucken, um

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

Der Ausgang ist interessant! (Es mag wie 21334 sein ...)

Die einzige Lösung ist, lokale Variablen zu verwenden.

+0

Das scheint mir nicht zu helfen. Immer noch nicht deterministisch. –

8

Hinter den Kulissen generiert der Compiler eine Klasse, die den Abschluss für Ihren Methodenaufruf darstellt. Es verwendet diese einzelne Instanz der Closure-Klasse für jede Iteration der Schleife.Der Code sieht wie folgt aus etwas, was es leichter zu sehen, warum der Fehler macht passiert:

void Main() 
{ 
    List<Func<int>> actions = new List<Func<int>>(); 

    int variable = 0; 

    var closure = new CompilerGeneratedClosure(); 

    Func<int> anonymousMethodAction = null; 

    while (closure.variable < 5) 
    { 
     if(anonymousMethodAction == null) 
      anonymousMethodAction = new Func<int>(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 Ihrer Probe, aber ich habe meinen eigenen Code untersucht und das sieht sehr viel wie der Compiler tatsächlich erzeugen würde.