V tomto článku si ukážeme a budeme podrobněji diskutovat jednu ze zajímavých záležitosti, na které bychom mohli narazit při používání async/await v praxi.
Mějme tento kód:
public abstract class DialogWindow : UserControl
{
/// <summary>
/// Akce volaná před zobrazením dialogu
/// </summary>
protected virtual Task Initialize()
{
//TODO: v základní třídě nechceme ale provádět nic
}
public async void Show()
{
//Dáme dialogu možnost provést (asynchronní) inicializaci ještě před vlastním zobrazením dialogu
await Initialize();
//...
}
}
public class FooDialog : DialogWindow
{
protected override async Task Initialize()
{
await LoadData();
//...
}
}
Zde třída DialogWindow představuje základní třídu pro obecný dialog. V této třídě chceme zavést metodu Initialize sloužící pro volání případných inicializačních akcí, které se mají provést ještě předtím než je vlastní dialog zobrazen (metodou Show). Pozor ale, že v implementaci metody Initialize konkrétního dialogu chceme umožnit volání i asynchronních akcí, proto tuto metodu zavedeme jako:
Task Initialize()
Jak jsme si již dříve vysvětlili tato signatura nám umožňuje v případě potřeby napsat tělo metody v odvozené třídě jako asynchronní “workflow” pomoci klíčového slova async a v metodě Show “počkat” pomoci klíčového slova await na dokončení asynchronně spouštěných akcí.
Protože obecně může existovat dialog, který žádnou inicializaci potřebovat nebude, měla by být v základní třídě pouze nějaká “prázdná implementace”. U ní budeme zároveň chtít, aby podporovala “fast path” tj. aby se při await nepřerušil běh volající metody a pokračovalo se hned akcemi za await (metoda Show tedy proběhne v takovém případě celá synchronně).
Jak tuto “prázdnou implementaci” udělat?
Možná by někoho napadlo, zda by nebylo možné použit klíčové slovo async a prázdného těla metody tj. deklarovat metodu Initialize takto:
protected virtual async Task Initialize() { /*warning CS1998*/ }
Takový kód se sice zkompiluje, ale způsobí warning CS1998:
This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
Který nám sice oznamuje, že protože metoda neobsahuje klíčové slovo await, bude se celá vykonávat synchronně – což my chceme – ale také, že klíčové slovo async by jsme měli použít pouze, pokud metoda obsahuje klíčové slovo await (klíčové slovo async by tam také v podstatě ani být nemuselo).
Takže takto ne.
Korektní řešení je v metodě Initialize vytvořit a vrátit novou instanci třídy Task, libovolného ale již dokončeného (podmínka pro “fast path”) Tasku.
To lze zajistit několika způsoby, například lze použit třídu TaskCompletionSource<TResult>:
protected virtual Task Initialize()
{
var tcs = new TaskCompletionSource<object>();
tcs.SetResult(null);
return tcs.Task;
}
Všimněte si, že existuje pouze generická verze třídy TaskCompletionSource<TResult>. Protože naše asynchronní akce ale nemá žádný výsledek (jen chceme umět “počkat” na její dokončení), je zde doporučováno (*) použít TaskCompletionSource<object> a hodnotu null.
To je ale jen opis toho co dělá pomocná metoda Task.FromResult<TResult> (nebo TaskEx.FromResult<TResult> v případě Async Targeting Pack pro Silverlight 5.0/.NET 4.0):
protected virtual Task Initialize()
{
return Task.FromResult(true);
}
Opět existuje pouze generická verze metody FromResult<TResult>, zde je ale jednodušší použít FromResult<bool> s hodnotou například true, případně FromResult<int> a hodnotou 0, aby zafungovala type inference.
(*) Oficiální dokumentacek tomuto konkrétně uvádí:
There is no non-generic counterpart to TaskCompletionSource<TResult>. However, Task<TResult> derives from Task, and thus the generic TaskCompletionSource<TResult> can be used for I/O-bound methods that simply return a Task by utilizing a source with a dummy TResult (Boolean is a good default choice, and if a developer is concerned about a consumer of the Task downcasting it to a Task<TResult>, a private TResult type may be used).
Nicméně použití typu object a hodnoty null je jednodušší způsob, který nevrací žádnou informaci.