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



在实现返回抛出异常的Task的方法时,是否存在MS"最佳实践"或合同协议?这是在编写单元测试时出现的,我正试图弄清楚是否应该测试/处理这种情况(我知道答案可能是"防御性编码",但我不希望这是答案)。

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

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

  3. 方法必须始终返回一个Task,除非开发人员耍无赖并执行他/她想要的操作(jk)。

Task Foo1Async(string id){
if(id == null){
throw new ArgumentNullException();
}
// do stuff
}
Task Foo2Async(string id){
if(id == null){
var source = new TaskCompletionSource<bool>();
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<bool>();
source.SetException(ex);
return source.Task;
}
}

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

处理异步方法的同步部分的异常。

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

如果该方法没有async签名,则可能会在调用方的堆栈帧上引发异常。

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

如果您真的需要知道任务是否已同步完成,您可以随时检查其Task.Completed/Faulted/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<T> and Task 
}
}
catch (Exception e)
{
// handle exceptions from synchronous part of Foo1Async,
// if it doesn't have `async` signature 
Debug.Print(e.ToString())
throw;
}

然而,通常情况下,您应该只awaitresult,而不关心任务是同步完成还是异步完成,以及可能抛出了哪个部分。任何异常都将在调用方上下文上重新抛出:

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

这也适用于单元测试,只要async方法返回Task(单元测试引擎不支持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的调用者处理异常,而不是手动捕获和传播它们。

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

然而,我自己的观点是"没关系"。考虑一下Eric Lippert的异常分类法。我们都同意,应该将外部异常放在返回的Task上(而不是直接在调用方的堆栈帧上抛出)。应完全避免出现Vexing异常。唯一有问题的异常类型是愚蠢的异常(例如,参数异常)。

我的论点是,它们如何抛出并不重要,因为您不应该编写捕获它们的生产代码。您的单元测试是唯一应该捕获ArgumentException和朋友的代码,如果您使用await,那么它们何时抛出也没关系。

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

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

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

最新更新