Wczoraj mówiłem o nowej funkcji „asynchronicznej” w języku C #, w szczególności o tym, jak wyglądał wygenerowany kod i the GetAwaiter()
/ BeginAwait()
/ EndAwait()
wywołania.
Przyjrzeliśmy się szczegółowo automatowi stanu wygenerowanemu przez kompilator C # i nie mogliśmy zrozumieć dwóch aspektów:
- Dlaczego wygenerowana klasa zawiera
Dispose()
metodę i$__disposing
zmienną, które nigdy nie są używane (a klasa nie jest implementowanaIDisposable
). - Dlaczego
state
zmienna wewnętrzna jest ustawiona na 0 przed jakimkolwiek wywołaniem funkcjiEndAwait()
, skoro 0 zwykle oznacza „to jest początkowy punkt wejścia”.
Podejrzewam, że na pierwszy punkt można odpowiedzieć, robiąc coś bardziej interesującego w ramach metody asynchronicznej, chociaż jeśli ktoś ma jakieś dodatkowe informacje, z przyjemnością je usłyszę. To pytanie dotyczy jednak bardziej drugiego punktu.
Oto bardzo prosty przykładowy kod:
using System.Threading.Tasks;
class Test
{
static async Task<int> Sum(Task<int> t1, Task<int> t2)
{
return await t1 + await t2;
}
}
... a oto kod, który jest generowany dla MoveNext()
metody implementującej maszynę stanową. To jest kopiowane bezpośrednio z Reflectora - nie naprawiłem niewypowiedzianych nazw zmiennych:
public void MoveNext()
{
try
{
this.$__doFinallyBodies = true;
switch (this.<>1__state)
{
case 1:
break;
case 2:
goto Label_00DA;
case -1:
return;
default:
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
break;
}
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
this.<>1__state = 2;
this.$__doFinallyBodies = false;
if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
Label_00DA:
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
this.<>1__state = -1;
this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
}
catch (Exception exception)
{
this.<>1__state = -1;
this.$builder.SetException(exception);
}
}
Jest długa, ale ważne linie na to pytanie są następujące:
// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
W obu przypadkach stan jest później ponownie zmieniany, zanim zostanie wyraźnie zaobserwowany ... więc po co w ogóle ustawiać go na 0? Gdyby MoveNext()
został wywołany ponownie w tym momencie (bezpośrednio lub przez Dispose
), skutecznie uruchomiłby ponownie metodę asynchroniczną, co byłoby całkowicie niewłaściwe, o ile mogę powiedzieć ... jeśli i MoveNext()
nie zostanie wywołane, zmiana stanu jest nieistotna.
Czy jest to po prostu efekt uboczny ponownego wykorzystania przez kompilator kodu generowania bloków iteratora dla asynchronizacji, gdzie może mieć bardziej oczywiste wyjaśnienie?
Ważne zastrzeżenie
Oczywiście to tylko kompilator CTP. W pełni spodziewam się, że coś się zmieni przed ostatecznym wydaniem - a być może nawet przed kolejną wersją CTP. To pytanie w żaden sposób nie próbuje twierdzić, że jest to wada kompilatora C # lub coś podobnego. Próbuję tylko dowiedzieć się, czy istnieje subtelny powód, którego przegapiłem :)