非同期メソッド(async ~ await)の戻り値

非同期メソッド(先頭に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>となる。

以上、わかる範囲でまとめてみた。

参考ページ