これまでの非同期メソッドは void
/ Task
/ Task<T>
のいずれかを戻り値にしなければならないという制約がありました。
非同期メソッドが C# 5 で導入されてから早 4 年。もはやこれを「制約」と感じることはほとんどないくらい馴染んでしまっていますが、C# 7 でこの制約を取り払う機能が導入される予定です。ちなみにこの機能、公式には Arbitrary Async Returns とか Task-like (Task
っぽい) という名称で呼ばれているようです。
ValueTask<T>
ValueTuple<T>
の記事でも多少説明しましたが、参照型はヒープ領域というメモリ空間を使用します。このメモリ空間の解放には若干重た目のガベージコレクション処理が走ります。一方値型はスタック領域というメモリ空間を使用し、こちらはより軽量な解放処理となります。これまで提供されてきた Task
型は参照型ですが、値型である ValueTask
型を提供することでいくつかのケースでのパフォーマンス改善が見込まれます。この ValueTask
はすでに .NET Core に実装されています。
ValueTask
は今回の主題である Task-like の機能に準拠しているため、以下のような感じで非同期メソッドが書けるようになります。
async ValueTask<int> GetValueAsync()
{
await Task.Delay(1000);
return 123;
}
ValueTask
は内部で Task
を抱える実装をしているのですが、内包する Task
を使うケースと使わないケースがあります。ValueTask
にすることで効果が発揮されるのは、このうちの Task
を使わないケースです。たとえば、以下のように await
を通るか通らないかで変わります。
async ValueTask<int> DoSomethingAsync()
{
var useInternalTask = true;
if (useInternalTask)
{
await Task.Delay(1000);
return 123;
}
return 456;
}
参照型の Task
を利用しないコードパスを通ることが多いケースや、モデルの奥深くでだけ非同期処理が行われているような場合は ValueTask
型がパフォーマンスに大きく寄与すると思われます。
戻り値になれる型の条件
仮に MyTask
型を非同期メソッドの戻り値にするためには、まず以下のシグネチャを持つビルダークラスを実装している必要があります。
public class MyTaskBuilder
{
public static MyTaskBuilder Create() => new MyTaskBuilder();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{}
public void SetStateMachine(IAsyncStateMachine stateMachine){}
public void SetResult(){}
public void SetException(Exception exception){}
public MyTask Task => default(MyTask);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{}
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{}
}
コンパイラはダックタイピング的に判定するため、何かの型を継承する必要はありません。MyTask
と MyTaskBuilder
に当たる部分の型名は任意なので、そのおかげで Task-like とする (= 任意の型を戻り値にする) ことができます。
どうしてこんな Builder が必要なのかは非同期メソッドを逆コンパイルしてみれば雰囲気が分かるかと思います。4 年前に内部実装について書いているのでそちらも参考にしてください。
戻り値にしたい型自体に属性を付与
このビルダー型がある前提で、以下のように戻り値にしたい型に AsyncMethodBuilder
属性を付けます。プロジェクト全体に対して適用したい場合に利用します。
[AsyncMethodBuilder(typeof(MyTaskBuilder))]
public class MyTask
{}
static async MyTask TestAsync()
{
await Task.Delay(1000);
}
戻り値を変更したい非同期メソッド自体に属性を付与
この機能は将来実装される予定になっているものです。2016/10/10 現在では動作しません。こんな感じになるのでは?という推測で書いています。
非同期メソッドの戻り値をピンポイントでカスタムしたい場合には、メソッドに対して個別に AsyncMethodBuilder
属性を付与します。
[AsyncMethodBuilder(typeof(MyTaskBuilder))]
static async MyTask TestAsync()
{
await Task.Delay(1000);
}
型自体に属性を付与することは型定義がないできませんが、メソッドに付与する場合は型定義がなくてもできるのがポイントです。これにより既存の型に対して適用するための逃げ道ができます。
最小限の実装で書いてみる
先の条件を満たしつつ、ほぼほぼ最小限の実装で書いてみると以下のような感じになります。ちょっと意味わからん系かもしれませんが、このくらいしないと任意の型を非同期メソッドの戻り値にできません。若干というか結構ハードル高いです。
[AsyncMethodBuilder(typeof(MyTaskBuilder<>))]
public class MyTask<T>
{
private Task<T> Task { get; }
public T Result => this.Task.GetAwaiter().GetResult();
public MyTask(Task<T> task){ this.Task = task; }
}
public class MyTaskBuilder<T>
{
private TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
public static MyTaskBuilder<T> Create() => new MyTaskBuilder<T>();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
=> stateMachine.MoveNext();
public void SetStateMachine(IAsyncStateMachine stateMachine){}
public void SetResult(T result) => this.tcs.SetResult(result);
public void SetException(Exception exception) => this.tcs.SetException(exception);
public MyTask<T> Task => new MyTask<T>(this.tcs.Task);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
=> awaiter.OnCompleted(stateMachine.MoveNext);
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
=> awaiter.OnCompleted(stateMachine.MoveNext);
}
既存の型を戻り値にする
例えば、WinRT / UWP 系アプリケーションや Reactive プログラミングとの相互運用性を高めるため以下のようなことをしたい場合があるかもしれません。この場合は独自ビルダーを作成してメソッドに対して属性を設定することで解決できます。もちろん標準ライブラリが IObservable<T>
や IAsyncAction
に AcyncMethodBuilder
属性を付けてくれれば、このような対応は不要になります。
IObservable<T> に対応
static void Main()
{
IObservable<T> sequence = SampleAsync();
}
[AsyncMethodBuilder(typeof(IObservableBuilder<>))]
static async IObservable<T> SampleAsync()
{
await Task.Delay(100);
return 123;
}
public class IObservableBuilder<T>
{
public IObservable<T> Task => this.tcs.Task.ToObservable();
}
IAsyncAction に対応
static void Main()
{
IAsyncAction action = UwpAsync();
}
[AsyncMethodBuilder(typeof(IAsyncActionBuilder))]
static async IAsyncAction UwpAsync()
{
await Task.Delay(100);
}
public class IAsyncActionBuilder
{
public IAsyncAction Task => this.tcs.Task.AsAsyncAction();
}
世界で 5 人だけ
以下のディスカッションにあるのですが、この機能は C# 7 に入りそうなものの「使うのは世界で 5 人ぐらいのものだろう」と言われています。非同期処理を独自の型で表現したいケースがどれほどあるのかということだと思いますが、まずないでしょう。なので、何か特殊な差し込みをしたいなどのことがない限り、この機能のことを知っている必要はないと思いますw