介绍
在我的代码困惑了一段时间后,我发现异常并不一定通过ContinueWith传播:
int zeroOrOne = 1; Task.Factory.StartNew(() => 3 / zeroOrOne) .ContinueWith(t => t.Result * 2) .ContinueWith(t => Console.WriteLine(t.Result)) .ContinueWith(_ => SetBusy(false)) .LogExceptions();
在这个例子中,SetBusy行’重置’异常链,所以除了零异常不会被看到,随后在我的脸上吹起了“一个任务的异常没有被观察到…”
所以…我写了一个自己的一些扩展方法(吨不同的重载,但基本上都这样做):
public static Task ContinueWithEx(this Task task,Action<Task> continuation) { return task.ContinueWIth(t => { if(t.IsFaulted) throw t.Exception; continuation(t); }); }
多搜索一下,我发现了this博客文章,他在那里提出了一个类似的解决方案,但是使用了一个TaskCompletionSource(其中释义)如下所示:
public static Task ContinueWithEx(this Task task,Action<Task> continuation) { var tcs = new TaskCompletionSource<object>(); return task.ContinueWith(t => { if(t.IsFaulted) tcs.TrySetException(t.Exception); continuation(t); tcs.TrySetResult(default(object)); }); return tcs.Task; }
题
这两个版本是否严格等同?或者在t.Exception和tcs.TrySetException(t.Exception)之间有微妙的区别?
另外,事实上,整个互联网上显然只有一个人是这样做的,这表明我错过了这样做的惯用方式?
解决方法
两者之间的区别是微妙的.在第一个示例中,您将抛出该任务返回的异常.这将触发正常的异常在CLR中抛出和捕获,ContinueWith将捕获并包装它并将其传递给链中的下一个任务.
在第二个你调用TrySetException,它仍将包装异常并将其传递给链中的下一个任务,但不会触发任何try / catch逻辑.
一个ContinueWithEx之后的最终结果是AggregateException(AggregateException(DivideByZeroException))).我唯一的区别是,内部的AggregateException在第一个例子中设置了一个堆栈跟踪(因为它被抛出),而在第二个例子中没有堆栈跟踪.
也不可能比另一个快得多,但我个人更喜欢第二个来避免不必要的投掷.
我做了这样的事情,继续返回结果.我调用它选择,处理前一个任务被取消的情况,提供重载来修改异常,而不是或除了结果,并使用ExecuteSynchronously选项.当继续本身返回一个Task时,我调用Then而不是基于this article的代码