前回は非同期メソッドの内部実装/展開のされ方を追いかけてみました。(当然ですが)コンパイラは一定の規約に沿ってこのような変換を行います。前回の内容を思い出しながら、非同期メソッドにするためのコンパイラ要件を見て行きましょう。
Awaitableになるためには
await演算子に渡すことができるインスタンスは、以下の条件を満たす必要があります。
- Awaiterを返すGetAwaiterメソッドを持つ
Awaiterは非同期処理の完了待機を補助するためオブジェクトです。ここで興味深いのは、返されるAwaiterの型は問われないということです。つまり、classでもstructでも構いませんし、クラス名も何でもOKです。また、GetAwaiterメソッドはインスタンスメソッドでも拡張メソッドでも構いません。メソッド名が同じでありさえすればコンパイルが通るという、ダックタイピング的な制約となっています。
public class Awaitable { public Awaiter GetAwaiter() { return new Awaiter(this); } }
public class Awaitable {} public static class AwaitableEx { public static Awaiter GetAwaiter(this Awaitable awaitable) { return new Awaiter(awaitable); } }
Awaiterになるためには
GetAwaiterメソッドで返されるAwaiterのインスタンスは、以下の条件を満たす必要があります。
- IsCompletedプロパティを持つ
- INotifyCompletionインターフェースを実装する
- GetResultメソッドを持つ
IsCompletedプロパティは、呼び出し時点で対象となる非同期処理が完了しているかどうかをbool値で返します。ここで「非同期処理が完了していない」と判定される場合、OnCompletedメソッドでの継続処理の登録に移ります。「完了している」とされる場合は、そのまま続きの処理が実行されます。
INotifyCompletionインターフェースは、OnCompletedメソッドの実装の義務付けです。引数に対象となる非同期処理の完了時に呼び出して欲しい継続処理を渡して、登録します。
GetResultメソッドでは非同期処理の結果として返される値を取得します。戻り値の型は問われません。voidでも独自型でもOKです。結果として戻す値がなくても戻り値がvoidのGetResultメソッドが必要な点は注意が必要です。また、戻り値がある場合はobject型で返すのではなくジェネリック型で返すと良いかと思います。
public class Awaiter<T> : INotifyCompletion { public bool IsCompleted{ get; } public void OnCompleted(Action continuation){} public T GetResult(){ return default(T); } }
.NET Framework標準で提供されているTaskAwaiter/TaskAwaiter<T>型はINotifyCompletionインターフェースを継承するICriticalNotifyCompletionインターフェースを実装しています。AwaiterがICriticalNotifyCompletionインターフェースを実装している場合は、コンパイラはOnCompletedメソッドではなくUnsafeOnCompletedメソッドを使うように展開するようです。とはいえ、現在OnCompletedメソッドとUnsafeOnCompletedメソッドをどのように使い分ければ良いのかは分かっていません...orz
独自awaitのための汎用性
このように、コンパイラはawaitするための条件を比較的緩い制約として設けています。この実装方法はAwaitableパターンと呼ばれているようです。.NET Framework標準で提供されているawait可能な型はTask/Task<T>だけですが、独自にawait可能な型を作ることができる汎用性があることを示しています。