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

时间:2014-02-09 16:14:21

标签: c# .net exception task-parallel-library async-await

在实现返回任务以抛出异常的方法时,是否存在MS“最佳实践”或合同协议?这是在编写单元测试时出现的,我试图弄清楚我是否应该测试/处理这种情况(我认识到答案可能是“防御性编码”,但我不希望这是答案)。< / p>

  1. 方法必须始终返回一个任务,该任务应包含抛出的异常。

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

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

  4. 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;
      }
    }
    

3 个答案:

答案 0 :(得分:6)

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

Handling exceptions from the synchronous part of async method

如果方法具有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;
}

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

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的来电者处理异常,而不是手动捕获和传播它们。

答案 1 :(得分:5)

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

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

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

答案 2 :(得分:3)

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

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

此答案假设您指的是Foo2Async返回未标有Task的方法。在那些你无法控制正在创建的任务的人中,任何异常都会自动存储在该任务中(所以这个问题就无关紧要了。)