Task や Task<TResult> の成功(完了)時のみ ContinueWith させたいと思ったことはないだろうか。
下記パターンとか・・・
Task(1つ目の処理)
.ContinueWith(成功時の処理)
.ContinueWith(成功時の処理)
・・・
.ContinueWith(エラーをまとめて処理)
TaskContinuationOptions.OnlyOnRanToCompletion を ContinueWith の引数に入れてやれば、成功時のみは可能なのだが、ContinueWith のメソッドチェーンで実装が難しくなる。
上記は、1st task 成功時のみ 2nd task → 3rd task の順で実行される。
1st task で例外が起こった場合、以降の Task は実行されず、
例外も未処理になってしまうため、task が GC される際にファイナライザで例外が発生する。(Webアプリとかで発生するとアプリが死ぬ奴)
※Task のエラー未処理対策は TaskScheduler.UnobservedTaskException
正しい方法としては、最初の Task を一度変数に格納して、そこから成功時・失敗時で別の Task に分離させていく。
→ 変数が増えてコードが複雑になる
では、ContinueWith 内で Task.IsFaulted なら Task.Exception を投げればいいかも?となるかもしれないが、ContinueWith 内に毎回同じ処理を書くのは面倒。
というわけで、次の拡張メソッドを作成。
また、このメソッドはエラーを次の Task に流すため、次の効果がある。
下記パターンとか・・・
Task(1つ目の処理)
.ContinueWith(成功時の処理)
.ContinueWith(成功時の処理)
・・・
.ContinueWith(エラーをまとめて処理)
TaskContinuationOptions.OnlyOnRanToCompletion を ContinueWith の引数に入れてやれば、成功時のみは可能なのだが、ContinueWith のメソッドチェーンで実装が難しくなる。
var task = Task.Factory.StartNew(() => {
Console.WriteLine("1st task");
/* 略 */
return 0;
}).ContinueWith(t => {
// 1st task 成功時のみ
Console.WriteLine("2nd task");
/* 略 */
return 0;
}, TaskContinuationOptions.OnlyOnRanToCompletion).ContinueWith(t => {
// 前 task の続き(つまり、1st task 成功時のみ)
Console.WriteLine("3rd task");
/* 略 */
return t.Result;
});
上記は、1st task 成功時のみ 2nd task → 3rd task の順で実行される。
1st task で例外が起こった場合、以降の Task は実行されず、
例外も未処理になってしまうため、task が GC される際にファイナライザで例外が発生する。(Webアプリとかで発生するとアプリが死ぬ奴)
※Task のエラー未処理対策は TaskScheduler.UnobservedTaskException
正しい方法としては、最初の Task を一度変数に格納して、そこから成功時・失敗時で別の Task に分離させていく。
→ 変数が増えてコードが複雑になる
では、ContinueWith 内で Task.IsFaulted なら Task.Exception を投げればいいかも?となるかもしれないが、ContinueWith 内に毎回同じ処理を書くのは面倒。
というわけで、次の拡張メソッドを作成。
解決案
///
/// task 成功時のみ completionFunc を実行する。
/// エラー・キャンセル情報は、次の Task に渡される。
/// beforeHandler が指定された(null でない)場合、task の Status に関わらず、最初に実行される。
///
public static Task<TOut> OnSuccess<TIn, TOut>(
this Task<TIn> task, Func<TIn, TOut> completionFunc, Action<Task<TIn>> beforeHandler)
{
return task.ContinueWith(t => t.OnSuccessImpl(completionFunc, beforeHandler))
.Unwrap();
}
///
/// OnSuccess の実装。
/// Task 内で OnSuccess を使う場合は、こちらを使用すると Task ネストが深くなりにくい。
///
public static Task<TOut> OnSuccessImpl<TIn, TOut>(
this Task<TIn> task, Func<TIn, TOut> completionFunc, Action<Task<TIn>> beforeHandler)
{
if( beforeHandler != null ) beforeHandler(task);
var tcs = new TaskCompletionSource<TOut>();
switch( task.Status ){
case TaskStatus.Canceled:
tcs.TrySetCanceled();
break;
case TaskStatus.Faulted:
tcs.TrySetException(task.Exception.InnerExceptions);
break;
case TaskStatus.RanToCompletion:
TOut result;
try {
result = completionFunc(task.Result);
} catch( Exception ex ){
tcs.TrySetException(ex);
break;
}
tcs.TrySetResult(result);
break;
}
return tcs.Task;
}
[使用例]
var task = Task.Factory.StartNew(() => {
Console.WriteLine("1st task");
var len = ((string)null).Length; //例外発生
return 0;
}).OnSuccess(t => {
// 1st task 成功時のみ
Console.WriteLine("success task");
return 0;
}, null).ContinueWith(t => {
// 必ず実行
Console.WriteLine("error handling");
return t.Result;
});
Console.WriteLine(task.Result);
//[結果]
// 1st task
// error handling
//
// 1st task の例外メッセージ
また、このメソッドはエラーを次の Task に流すため、次の効果がある。
- エラー未処理バグが減る
- AggregateException が深くならない
0 件のコメント:
コメントを投稿