C# 5.0で搭載された非同期メソッド (async/await) は、非同期処理を非常に簡単に記述できる言語機能として大変重宝し、愛されています。await演算子は通常のメソッドだけでなくラムダ式内でも利用できるなど、かなり広範囲への適用が可能です。しかし例外処理をする場合はtry句の中でしかawait演算子を利用できないという大きな制約がありました。
例えばWindowsストアアプリの開発時、例外が発生した際にメッセージを表示しようとした場合に以下のようなコードが書けませんでした。
async void Button_Click(object sender, RoutedEventArgs e) { try { //--- 何か適当な非同期処理 await Task.Delay(3000); } catch (Exception ex) { var dialog = new MessageDialog(ex.Message, "エラー発生"); await dialog.ShowAsync(); //--- コンパイルエラー } }
なので、大体以下のような書き方で回避してきたと思います。
async void Button_Click(object sender, RoutedEventArgs e) { Exception exception = null; try { //--- 何か適当な非同期処理 await Task.Delay(3000); } catch (Exception ex) { exception = ex; } //--- メッセージ表示を外出し if (exception != null) { var dialog = new MessageDialog(exception.Message, "エラー発生"); await dialog.ShowAsync(); } }
C# 6.0ではこの制約が取り払われ、catch句/finally句でもawait演算子を利用できるようになりました。非常に素晴らしい改善です。
逆コンパイル
非同期メソッド入門 (7) – 内部実装を覗くで、async/awaitは糖衣構文であることを紹介しました。今回行われたcatch句/finally句への拡張はどのように展開されるのか。逆コンパイルをして試してみました。
using System; using System.Threading.Tasks; namespace CSharpVNext { class Program { static void Main() { DoSomethingAsync().Wait(); } static async Task DoSomethingAsync() { try { await Task.Run(() => Console.WriteLine("try")); } catch { await Task.Run(() => Console.WriteLine("catch")); } finally { await Task.Run(() => Console.WriteLine("finally")); } } } }
上記のコードを逆コンパイルすると以下のようになります。変数名やラベル名などはもっとヒドい状態で展開されますが、読み易さのために意味が変わらない範囲で大きく改変しています。その点はご了承ください。
using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace CSharpVNext { class Program { [CompilerGenerated] [StructLayout(LayoutKind.Auto)] private struct DoSomethingAsyncStateMachine : IAsyncStateMachine { public int state; public AsyncTaskMethodBuilder builder; public Exception catchStatementException; public bool isExceptionOccuredInTryStatement; public TaskAwaiter awaiter; //--- 非同期メソッドの本体 void IAsyncStateMachine.MoveNext() { //--- この関数が呼び出されたときの初期状態を覚えておく var cachedState = this.state; try { //--- 非同期処理完了後に処理が継続されるように適切な位置へ飛ぶ TaskAwaiter taskAwaiter; switch (cachedState) { case 1: case 2: goto Start; case 3: taskAwaiter = this.awaiter; this.awaiter = default(TaskAwaiter); this.state = -1; goto FinallyAwaitCompleted; } this.catchStatementException = null; Start: try { //--- try句の非同期処理が実行された直後以外 if (cachedState != 1) { //--- catch句の非同期処理が実行された直後 if (cachedState == 2) { taskAwaiter = this.awaiter; this.awaiter = default(TaskAwaiter); this.state = -1; goto CatchAwaitCompleted; } this.isExceptionOccuredInTryStatement = false; } try { if (cachedState != 1) //--- try句の非同期処理が実行されていないとき { //--- try句の非同期処理を実行 taskAwaiter = Task.Run(() => Console.WriteLine("try")).GetAwaiter(); if (!taskAwaiter.IsCompleted) { //--- 非同期処理が完了していない場合 //--- 1. 状態を記憶 / 2. コールバックを設定 / 3. 処理を戻す this.state = 1; this.awaiter = taskAwaiter; this.builder.AwaitUnsafeOnCompleted<TaskAwaiter, DoSomethingAsyncStateMachine>(ref taskAwaiter, ref this); return; } } else //--- try句の非同期処理が実行された後 { taskAwaiter = this.awaiter; this.awaiter = default(TaskAwaiter); this.state = -1; } taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter); } catch { //--- try句で例外が発生したことをメモ this.isExceptionOccuredInTryStatement = true; } //--- try句で例外が発生しなかった場合はfinally句へ if (this.isExceptionOccuredInTryStatement) goto BeforeFinally; //--- catch句の非同期処理を実行 taskAwaiter = Task.Run(() => Console.WriteLine("catch")).GetAwaiter(); if (!taskAwaiter.IsCompleted) { this.state = 2; this.awaiter = taskAwaiter; this.builder.AwaitUnsafeOnCompleted<TaskAwaiter, DoSomethingAsyncStateMachine>(ref taskAwaiter, ref this); return; } //--- catch句の非同期処理が完了した後の処理 CatchAwaitCompleted: taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter); //--- finally句の処理前 BeforeFinally: ; } catch (Exception ex) { //--- catch句で発生した例外をメモ this.catchStatementException = ex; } //--- finally句の非同期処理を実行 taskAwaiter = Task.Run(() => Console.WriteLine("finally")).GetAwaiter(); if (!taskAwaiter.IsCompleted) { this.state = 3; this.awaiter = taskAwaiter; this.builder.AwaitUnsafeOnCompleted<TaskAwaiter, DoSomethingAsyncStateMachine>(ref taskAwaiter, ref this); return; } //--- finally句の非同期処理が完了した後の処理 FinallyAwaitCompleted: taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter); if (this.catchStatementException != null) ExceptionDispatchInfo.Capture(this.catchStatementException).Throw(); //--- catch句で発生した例外をスロー this.catchStatementException = null; } catch (Exception ex) { //--- catch句 or finally句で発生した例外を非同期メソッドの結果とする this.state = -2; this.builder.SetException(ex); return; } //--- 正常終了 this.state = -2; this.builder.SetResult(); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { this.builder.SetStateMachine(stateMachine); } } static void Main() { DoSomethingAsync().Wait(); } [DebuggerStepThrough, AsyncStateMachine(typeof(DoSomethingAsyncStateMachine))] static Task DoSomethingAsync() { var machine = new DoSomethingAsyncStateMachine(); machine.builder = AsyncTaskMethodBuilder.Create(); machine.state = -1; machine.builder.Start<DoSomethingAsyncStateMachine>(ref machine); return machine.builder.Task; } } }
非常に上手く展開されています。しかし、なぜC# 5.0のときに最初から同じようにやらなかったのか疑問ではあります...。