实现返回Task的方法时的合同协议

在实现返回关于抛出exception的Task的方法时,是否存在MS“最佳实践”或合同协议? 这是在编写unit testing时出现的,我试图弄清楚我是否应该测试/处理这种情况(我认识到答案可能是“防御性编码”,但我不希望这是答案)。

  1. 方法必须始终返回一个Task,该Task应包含抛出的Exception。

  2. 方法必须始终返回一个Task,除非该方法提供无效参数(即ArgumentException)。

  3. 方法必须总是返回一个任务,除非开发人员变得流氓并做他/她想要的事情(jk)。

Task Foo1Async(string id){ if(id == null){ throw new ArgumentNullException(); } // do stuff } Task Foo2Async(string id){ if(id == null){ var source = new TaskCompletionSource(); source.SetException(new ArgumentNullException()); return source.Task; } // do stuff } Task Bar(string id){ // argument checking if(id == null) throw new ArgumentNullException("id") try{ return this.SomeService.GetAsync(id).ContinueWith(t => { // checking for Fault state here // pass exception through. }) }catch(Exception ex){ // handling more Fault state here. // defensive code. // return Task with Exception. var source = new TaskCompletionSource(); source.SetException(ex); return source.Task; } } 

我最近问过一个类似的问题:

处理异步方法的同步部分中的exception 。

如果该方法具有async签名,则抛出方法的同步或异步部分无关紧要。 在这两种情况下,exception都将存储在Task 。 唯一的区别是,在前一种情况下,生成的Task对象将立即完成(出现故障)。

如果该方法没有async签名,则可能会在调用者的堆栈帧上抛出exception。

IMO,在任何一种情况下,调用者都不应该对是否从同步或异步部分抛出exception,或者该方法是否具有async签名做出任何假设

如果您确实需要知道任务是否已同步完成,则可以始终检查其Task.Completed / Task.Completed / Cancelled状态或Task.Exception属性,而无需等待:

 try { var task = Foo1Async(id); // check if completed synchronously with any error // other than OperationCanceledException if (task.IsFaulted) { // you have three options here: // 1) Inspect task.Exception // 2) re-throw with await, if the caller is an async method await task; // 3) re-throw by checking task.Result // or calling task.Wait(), the latter works for both Task and Task } } catch (Exception e) { // handle exceptions from synchronous part of Foo1Async, // if it doesn't have `async` signature Debug.Print(e.ToString()) throw; } 

但是, 通常你应该await result ,而不关心任务是否同步或异步完成,以及哪个部分可能抛出。 将在调用者上下文中重新抛出任何exception:

 try { var result = await Foo1Async(id); } catch (Exception ex) { // handle it Debug.Print(ex.ToString()); } 

这也适用于unit testing,只要async方法返回一个Task (unit testing引擎不支持async void方法,AFAIK,这是有意义的:没有Task可以跟踪和await )。

回到你的代码,我会这样说:

 Task Foo1Async(string id){ if(id == null) { throw new ArgumentNullException(); } // do stuff } Task Foo2Async(string id) { if(id == null){ throw new ArgumentNullException(); } // do stuff } Task Bar(string id) { // argument checking if(id == null) throw new ArgumentNullException("id") return this.SomeService.GetAsync(id); } 

Foo1AsyncFoo2AsyncBar的调用者处理exception,而不是手动捕获和传播它们。

我知道Jon Skeet喜欢在单独的同步方法中进行前置条件检查,以便直接抛出它们。

但是,我自己的观点是“无所谓”。 考虑Eric Lippert的exception分类 。 我们都同意外部exception应放在返回的Task (不直接抛出调用者的堆栈帧)。 应完全避免烦恼的例外情况。 所讨论的唯一类型的例外是愚蠢的例外(例如,参数例外)。

我的论点是,抛出它们并不重要,因为你不应该编写能够捕获它们的生产代码 。 你的unit testing是唯一应该捕获ArgumentException和朋友的代码,如果你使用await那么它们何时被抛出并不重要。

方法返回任务的一般情况是因为它们是异步方法。 在这些情况下,通常应该抛出方法的同步部分中的exception,就像在任何其他方法中一样,并且在异步部分中应该存储在返回的Task内部(通过调用async方法或匿名委托自动)。

因此,在像无效参数这样的简单情况下,只需像Foo1Async一样抛出exception。 在有关异步操作的更复杂的情况下,在Foo2Async为返回的任务设置了一个exception

这个答案假设您指的是未标记为async Task返回方法。 在那些你无法控制正在创建的任务的情况下,任何exception都会自动存储在该任务中(因此问题将无关紧要)。