未实现/不支持/无效操作的异步方法。

19
什么是标记异步方法为未实现/不支持或无效操作的正确方式?为简单起见,我将在示例中仅使用 NotImplementedException,但该问题也适用于NotSupportedExceptionInvalidOperationException
同步地,我们可以简单地抛出异常:
public override void X() {
    throw new NotImplementedException();
}

这段代码在异步世界中的等效代码是什么?
/* 1 */ public override Task XAsync() {
    throw new NotImplementedException();
}

或者

/* 2 */ public override Task XAsync() {
    return Task.FromException(new NotImplementedException());
}

这些方法有哪些并发症?有更好的方法吗?
为了避免出现“不需要在这里使用异步方法”或“这不是异步的”这样的情况,我会说该方法实现了某个接口或抽象类。

我不考虑的一些方法:

/* 3 */ public async override Task XAsync() { // here is an CS1998 warning 
    throw new NotImplementedException();
}

编译器 只会生成无用的状态机,其语义与2等价。

/* 4 */ public async override Task XAsync() {
    await Task.Yield();
    throw new NotImplementedException();
}

这是与3相同的代码,只不过在Task.Yeild()上添加了await关键字。

3
好问题。第一种选项在方法被调用时抛出异常,第二种选项在结果被等待时抛出异常。我更倾向于第一种选项,因为它能够“早早失败”,但我很想知道专家们会有什么看法。 - Heinzi
也许这可以帮助你:https://dev59.com/92Yr5IYBdhLWcg3w--0I#13254787 - Felipe Oriani
是的,@FelipeOriani 看到了,谢谢。 - hazzik
你提供的两个选项都不是“async”方法;这是另一个需要考虑的选项。 - user743382
1
@hazzik 是的。你没有理解我的评论吗?我是说 async Task XAsync() { throw ...; } 是另一个选项。使用 async 关键字。 - user743382
显示剩余4条评论
3个回答

10

我要冒昧地说一句:“这并不重要。”

愚蠢的异常可以直接抛出(throw)或放置在返回的任务上(Task.FromException)。由于它们是愚蠢的异常,它们本来就不应该被捕获,所以无论它们在哪里抛出都没有关系。


5
当调用返回 Task 的方法时,其中的一些部分会同步执行(即使实现方法被定义为 async 并且其中有 await 调用..在第一个 await 之前,所有内容都是默认同步执行的)。
因此,所有选项的结果都是相同的:立即抛出异常或返回已经完成的异常任务(只有在立即等待调用时才表现得相同),或者标记一个方法为 async(这需要你具备 await 调用,但是为了完整性我们将其添加)。
我建议立即抛出异常,因为返回 Task 可能表示“已经开始工作”,而如果调用者不真正关心你的 Task 何时完成(它甚至没有返回值),那么方法未实现的事实就不会显示出来。

虽然我倾向于这样做,但它无法构建,因为没有等待。 - micahhoover
1
如果你只是在一个返回任务的方法中抛出异常,不要使用 async 关键字来定义你的方法。 - Martin Ullrich
1
在我们必须使用的框架中,抽象方法是异步的。我们选择使用 => await Task.FromException<NotImplementedException>(new NotImplementedException()); 来满足编译器警告和抽象方法签名。 - clarkitect

3
在你的评论中,你写道:
我们正在尝试制作 NHibernate 的异步版本 :)
这将使你处于一个不幸的位置:有经验的程序员编写的知名库应该编写良好,以保护那些经验较少的程序员免受意外滥用(包括通过复制/粘贴进行滥用)。
有足够多的人认为像 await Task.WhenAll(a(), b(), c()) 这样的代码应该能正常工作,即使其中一个异步操作失败了,所以我认为你的第一选择甚至不应该被考虑。如果 b() 同步地抛出异常,则会忽略 a() 返回的任务,并且不会调用 c() 。
我同意 Stephen Cleary 的答案,他说 NotImplementedException 是一个愚蠢的异常,不重要,因为它不应该出现在生产代码中。然而,你写道:
问题也适用于 NotSupportedException 和 InvalidOperationException 。
这些并不一定是愚蠢的异常。这些异常可能会出现在生产代码中。
你的第二个选项避免了这个问题。
你的第二个选项确实有一个额外的问题:它不会抛出异常。由于实际上没有抛出异常,这会阻碍调试:当出现问题时,在断点处停止抛出异常比在捕获异常处停止更有用。
我建议考虑另外一种方法。
public async override Task XAsync() {
  throw new NotImplementedException();
}

由于创建状态机时涉及的开销,您放弃了此选项。我认为这不是放弃它的有效理由。在这段代码中,性能并不重要,只处理错误情况。我建议使用async/await而不是直接操作任务,因为更容易捕获愚蠢的错误,并且节省的开发时间足以值得这样做。
我理解您为什么不想选择此选项,但我个人仍然会选择它。它避免了第一个选项的缺点,也避免了第二个选项的缺点。它自己的缺点,稍微降低的性能,在我的经验中比其他两个选项更少成为问题。
希望上述缺点的细节可以帮助您做出明智的决策。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接