【问题标题】:Contract agreement when implementing a method that returns a Task实现返回任务的方法时的合同协议
【发布时间】:2014-02-09 16:14:20
【问题描述】:

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

  1. 方法必须总是返回一个Task,它应该包含抛出的异常。

  2. 方法必须始终返回一个任务,除非方法提供了无效参数(即 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<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;
  }
}

【问题讨论】:

  • 有趣的问题。我从未见过这样的指导方针,所以我很想说 4:方法可能会抛出异常或返回包含该异常的任务,调用者应该将这些情况视为等效。
  • @hvd 防御性编码建议调用者处理这两种情况,但我的极简主义者不想处理以多种方式表达的异常。例外是参数检查(有点自相矛盾)。
  • 但同时,抛出异常的方式不应取决于该方法是否使用async 实现,因为这是调用者不可见的实现细节。如果您同意这一点,那么这两种情况应该被视为等效,或者您永远不应该同步抛出异常。

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


【解决方案1】:

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

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

但是,通常你应该只是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的调用者处理异常,而不是手动捕获和传播它们。

【讨论】:

    【解决方案2】:

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

    不过,我个人的看法是“没关系”。考虑 Eric Lippert 的exception taxonomy。我们都同意应该将外生异常放在返回的Task 上(而不是直接在调用者的堆栈帧上抛出)。应该完全避免令人烦恼的异常。唯一有问题的异常类型是愚蠢的异常(例如,参数异常)。

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

    【讨论】:

    • 在 OP 的情况下,this.SomeService.GetAsync() 内部可能抛出的异常是否会被认为是外生的(我认为是这样)?然后是否应该用TCS.SetException 包装,就像在那里完成的那样?
    • 是的,这是外生的。我将使用async/await 实现Bar,这将在幕后使用TCS.SetException
    • 如果您能展示Bar两者 TaskCompletionSourceasync/await 的外观,我将不胜感激。我觉得我错过了一些明显的东西。谢谢!
    • 啊,所以它是要么 TCS 或async/await 这样pastebin.com/ncXH3T9X 吗?
    • @Noseratio:是的,我就是这个意思。
    【解决方案3】:

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

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

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

    【讨论】:

    • +1。同意。参数验证之类的东西没有理由等待。如果不能立即扔掉,那就投入到任务中。
    • 我用 Bar(string):Task 更新了这个例子。 Bar 依赖于一些返回任务的服务。该示例应该在 2 个区域(try block 和 ContinueWith)中处理异常/故障状态。
    • @Kevin 是的。这就是我的意思
    • @l3arnon: 在这些情况下,通常应该像在任何其他方法中一样抛出方法的同步部分中的异常,并且应该将异步部分中的异常存储在返回的内部任务。 无论您是从async 方法的同步部分还是异步部分抛出。在这两种情况下,异常都将存储在Task 中。唯一的区别是Task 对象将在前一种情况下立即完成(故障)。如果async方法从同步部分抛出,但调用者没有await返回任务,则异常不会被重新抛出。
    • @Noseratio 示例中的方法是异步的,但没有用async 标记,这在您删除不必要的多余async 修饰符时很常见。
    猜你喜欢
    • 2016-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多