非同期メソッド(先頭にasyncが付き、処理内に一つ以上の awaitを含むメソッド)の戻り値の型は次の3つ。
以下に、それぞれについてメモ。
戻り値がvoidの場合
【例】
async void AsyncVoidMethod() { await doSomething(); }
非同期メソッドからは何も返されず、「Fire and forget」(撃ち放し)の処理となる。非同期メソッドから情報を受け取る手段がないので、
- 呼び出し元が非同期タスクの完了を待つことができない
- 非同期メソッドが投げた例外をキャッチできない
というデメリットがあるが、
- イベントハンドラ―に設定する場合
- void を返すメソッドをオーバーライドする場合
に使われる。
戻り値がTaskの場合
【例】
async Task AsyncTaskMethod() { await doSomthing(); }
戻り値にTaskが指定されているが、returnの記述がなく、メソッド本体は戻り値がvoidの場合と変わらない。
- 非同期メソッド(async ~ await)は本体にreturnがない場合Taskを返す(戻り値がvoidでもTaskでも本体の記述は変わらない)
- 呼び出し元は await を使うか、メソッドから返されたTaskを Wait()することで非同期処理の完了を待つことができる
- 完了を待っている非同期処理の中で例外が発生した場合、try-catchステートメントを使ってその例外をキャッチすることができる
voidとTaskのまぎらわしさ - 非同期メソッドがラムダ式で作られた場合 ー
そもそもこのページを書くきっかけは、ネットで見たコードを理解できなかったことだった。例えば下のコード。引数にメソッドを取る「sampleMethod」メソッドに、ラムダ式で作った非同期メソッドを与えているのだが…
【例】
sampleFunc(async () => { await Task.Delay(1000); });
この場合、ラムダ式の非同期メソッドはAction<void>とFunc<Task>、どちらの型になるだろうか?実は、どちらの型になることもできて、sampleFuncの引数がどう定義されているかによって、Action<void>とFunc<Task>、どちらか適切な型でコンパイルされる。
初めてこれを見たときは「非同期メソッド(async ~ await)は本体にreturnがない場合Taskを返す」ということがわかっていなかったので、どうしてもAction<void>になるとしか考えられなかった。
それに、メソッド本体が同じなのに2つの型を取りうるということもなかなか納得がいかなかった(varなんかと同じで「コンパイラが賢い」というだけのなのだが)。
戻り値がTask<T>の場合
【例】
async Task<T> AsyncTaskTMethod() { // Tという型の変数t T t; // doSomethingもTask<T>を返すメソッドであるとする // Task<T>を受け取るawait式はTと評価される t = await doSomething(); // メソッドの返り値はTask<T>だが、returnするのはTでいい return t; }
今度の例では、doSometing()もTask<T>を返すメソッドということにしている。Taks<T>を返す非同期メソッドをawaitした場合、awaitは非同期メソッドの完了後、Tとして評価される。
また、メソッド本体ではT型であるtをreturnしているだけだが、非同期メソッド(async ~ await)からTをreturnするとメソッドの戻り値はTask<T>となる。
以上、わかる範囲でまとめてみた。