2015-07-29 1 views
15

Während ich den Hund ging, dachte ich an Action<T>, , async/await (ja, nerdy, ich weiß ...) und konstruierte ein kleines Testprogramm in meinem Kopf und fragte mich, wie die Antwort sein würde. Ich bemerkte, dass ich mich über das Ergebnis nicht sicher war, also habe ich zwei einfache Tests erstellt.Warum wird eine Klassenbereichsvariable bei Verwendung einer asynchronen Methode erfasst, aber nicht bei Verwendung einer Aktion <T> (Codebeispiele im Inneren)?

Hier ist das Setup:

  • ich einen Klassenbereich Variable (string).
  • Es wird ein Anfangswert zugewiesen.
  • Die Variable wird als Parameter an eine Klassenmethode übergeben.
  • Die Methode wird nicht direkt ausgeführt, sondern einer 'Aktion' zugewiesen.
  • Bevor die Aktion ausgeführt wird, ändere ich den Wert der Variablen.

Was wäre der Ausgang? Der Anfangswert oder der geänderte Wert?

Ein wenig überraschend, aber verständlich, die Ausgabe ist der geänderte Wert. Meine Erklärung: Die Variable wird nicht auf den Stapel geschoben, bis die Aktion ausgeführt wird, also wird sie geändert.

public class foo 
{ 
    string token; 

    public foo() 
    { 
     this.token = "Initial Value"; 
    } 

    void DoIt(string someString) 
    { 
     Console.WriteLine("SomeString is '{0}'", someString); 
    } 

    public void Run() 
    { 
     Action op =() => DoIt(this.token); 
     this.token = "Changed value"; 
     // Will output "Changed value". 
     op(); 
    } 
} 

Als nächstes habe ich eine Variation:

public class foo 
{ 
    string token; 

    public foo() 
    { 
     this.token = "Initial Value"; 
    } 

    Task DoIt(string someString) 
    { 
     // Delay(0) is just there to try if timing is the issue here - can also try Delay(1000) or whatever. 
     return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", someString)); 
    } 

    async Task Execute(Func<Task> op) 
    { 
     await op(); 
    } 

    public async void Run() 
    { 
     var op = DoIt(this.token); 
     this.token = "Changed value"; 
     // The output will be "Initial Value"! 
     await Execute(() => op); 
    } 
} 

Hier machte ich DoIt() Rückkehr ein Task. op ist jetzt ein Task und nicht mehr ein Action. Die Methode Execute() erwartet die Aufgabe. Zu meiner Überraschung ist die Ausgabe jetzt "Anfangswert".

Warum verhält es sich anders?

DoIt() erst Execute() wird aufgerufen, ausgeführt werden, also warum es den Anfangswert von token nicht erfassen?

komplette Tests: https://gist.github.com/Krumelur/c20cb3d3b4c44134311f und https://gist.github.com/Krumelur/3f93afb50b02fba6a7c8

+0

In DoIt befindet sich das Capture in averyString, dem Methodenparameter, nicht im Tokenfeld. Da der Wert, den Sie an die Methode übergeben, "Anfangswert" ist, ist dies der Wert, den Sie sehen. –

+3

* DoIt() wird nicht ausgeführt, bis Execute() aufgerufen wird * - nicht wahr. Wenn Sie eine Aufgabe in einem 'asynchronen' Kontext instanziieren, wird sie sofort gestartet - sie wartet nicht, bis sie 'abgewartet' ist (so können Sie mehrere gleichzeitige Aufgaben erwarten). –

+0

@AntP Das ist natürlich richtig - und das weiß ich. Scheint, ich war so verblüfft über das unterschiedliche Verhalten, dass ich es vergessen habe. Mit Blick auf die Upvotes scheint die Frage recht interessant zu sein. – Krumelur

Antwort

7

Sie haben hier ein paar Missverständnisse. Erstens, wenn Sie DoIt aufrufen, gibt es eine Aufgabe zurück, die bereits mit der Ausführung begonnen hat. Ausführung nicht starten nur, wenn Sie await die Aufgabe.

Sie auch einen Verschluss über die someString Variable erstellen, dessen Wert nicht ändert, wenn Sie den Klasse-Level-Bereich zuweisen:

Task DoIt(string someString) 
{ 
    return Task.Delay(0).ContinueWith(t 
     => Console.WriteLine("SomeString is '{0}'", someString)); 
} 

Die Action-ContinueWith weitergegeben den someString Variable schließt. Denken Sie daran, dass die Zeichenfolgen unveränderlich sind. Wenn Sie also den Wert token neu zuweisen, weisen Sie tatsächlich eine neue Zeichenfolgenreferenz zu. Die lokale Variable someString innerhalb DoIt behält jedoch die alte Referenz bei, so dass ihr Wert auch nach der Neuzuweisung des Klassenfelds gleich bleibt.

Sie könnten dieses Problem lösen, indem sie stattdessen direkt diese Aktion dicht über die Klasse Ebene Feld mit:

Task DoIt() 
{ 
    return Task.Delay(0).ContinueWith(t 
     => Console.WriteLine("SomeString is '{0}'", this.token)); 
} 
+2

Das Instanziieren von Aufgaben hat das gleiche Verhalten innerhalb und außerhalb von Async-Methoden. Es gibt ein paar Unterschiede zwischen Async- und Nicht-Async-Methoden, aber dies gehört nicht dazu. Wenn Sie 'Task.Delay' in einer Nicht-Async-Methode aufrufen, wird die Ausführung sofort gestartet. – Luaan

+0

Ja - du bist genau da. Das habe ich behoben. –

5

In beiden Fällen Sie machen einen Verschluss. In beiden Fällen schließen Sie jedoch verschiedene Dinge.

Im ersten Fall sind Sie mit einem Verschluss über this eine anonyme Methode zu machen - wenn man endlich die Delegaten ausführt, wird es dauern, den aktuellen Wert von this, erhält den aktuellen Wert von this.token und Verwendung Das. Sie sehen also den geänderten Wert.

Im zweiten Fall gibt es keine Schließung über this - oder wenn es ist, macht es keinen Unterschied. Sie übergeben this.token explizit, und die DoIt Methode muss nur eine Schließung über sein eigenes Argument, someString. Dies geschieht sofort (synchron), anstatt faul - so wird der Anfangswert von this.token erfasst.await nicht wirklich führen Sie den Delegaten - es wartet nur auf die Ergebnisse der asynchronen Methode. Die Methode selbst ist bereits gelaufen, und nur ihr asynchroner Teil ist, na ja, asynchron - in diesem Fall nur die Console.WriteLine("SomeString is '{0}'", someString).

Wenn Sie mehr dies deutlich sehen möchten, fügen Sie Thread.Sleep(1000) nach this.token = "Changed value"; - Sie SomeString is 'Initial Value' ausgedruckt vor Sie selbst bekommen die await sehen.

Damit sich das zweite Beispiel wie das erste verhält, müssen Sie nur op als Delegat ändern, anstatt Task - . Dies verzögert die Ausführung von DoIt erneut und bewirkt den gleichen Abschluss wie im ersten Beispiel.

TL; DR:

Das Verhalten anders, weil im ersten Fall ist, verschieben Sie die Ausführung von DoIt(this.token), während im zweiten Beispiel, das Sie DoIt(this.token) sofort ausgeführt. Die anderen Punkte in meiner Antwort sind ebenfalls wichtig, aber das ist der Kernpunkt.

5

den jeden Fall brechen lassen.

Beginnend mit dem Action<T>:

Meine Erklärung: Die Variable wird nicht auf den Stapel geschoben, bis die Aktion ausführt, so wird es die geänderte ein

Das ist nicht alles hat sein mit dem Stapel zu tun haben. Der Compiler generiert die folgenden von Ihrem ersten Code-Schnipsel:

public foo() 
{ 
    this.token = "Initial Value"; 
} 

private void DoIt(string someString) 
{ 
    Console.WriteLine("SomeString is '{0}'", someString); 
} 

public void Run() 
{ 
    Action action = new Action(this.<Run>b__3_0); 
    this.token = "Changed value"; 
    action(); 
} 

[CompilerGenerated] 
private void <Run>b__3_0() 
{ 
    this.DoIt(this.token); 
} 

Der Compiler gibt eine benannte Methode aus dem Lambda-Ausdruck. Sobald Sie die Aktion aufrufen, und da wir in derselben Klasse sind, ist this.token der aktualisierte "Geänderte Wert".Der Compiler hebt dies nicht einmal in eine Anzeigeklasse auf, da dies alles innerhalb der Instanzmethode erzeugt und aufgerufen wird.


Jetzt für die async Methode. Es werden zwei Zustandsmaschinen erzeugt, die das Aufblähen der Zustandsmaschine erschweren und zu den relevanten Teilen gelangen. Die Zustandsmaschine macht folgendes:

this.<>8__1 = new foo.<>c__DisplayClass4_0(); 
this.<>8__1.op = this.<>4__this.DoIt(this.<>4__this.token); 
this.<>4__this.token = "Changed value"; 
taskAwaiter = this.<>4__this.Execute(new Func<Task>(this.<>8__1.<Run>b__0)).GetAwaiter(); 

Was passiert hier? token wird an DoIt übergeben, die eine Func<Task> zurückgibt. Dieser Delegat enthält einen Verweis auf die alte Token-Zeichenfolge "Initial Value". Denken Sie daran, obwohl wir über Referenztypen sprechen, werden sie alle nach Wert weitergegeben. Dies bedeutet effektiv, dass jetzt ein neuer Speicherort für den alten String in der DoIt-Methode ist, der auf "Anfangswert" zeigt. Dann ändert die nächste Zeile token zu "Changed Value". Die string gespeichert in der Func und die, die geändert wurde sind zeigt jetzt auf zwei verschiedene Zeichenfolgen.

Wenn Sie den Delegaten aufrufen, wird der Anfangswert gedruckt, da der Task op Ihren älteren, veralteten Wert speichert. Deshalb sehen Sie zwei verschiedene Verhaltensweisen.

+0

"zeigt nun auf zwei verschiedene Strings" - darauf hat auch @AntP hingewiesen. Es ist nur durch den umgebenden Code verdeckt. Danke, dass du die IL Version zeigst! – Krumelur

+0

@Krumelur Ich wollte dir zeigen, was eigentlich passiert. Dies ist nicht IL, das ist einfach der dekompilierte Code :) –

+0

True. Ich habe "IL" geschrieben, weil ich mich gefragt habe, was du benutzt hast, um zu dekompilieren und zu denken, vielleicht war es "** IL ** Spy" :-) – Krumelur

Verwandte Themen