为什么构造函数中的异步操作会吞噬异常?

Why is an exception swallowed in an async action in a constructor?

我认为我很聪明地找到了一种在 ctor 中调用 ync 方法的方法:

public AppStateModel(IBranchClient branchClient)
{
    _branchClient = branchClient;
    var loadBranch = new Action(async () =>
    {
        DataProviderReadResult<BranchDetailViewModel> result = await _branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);
    });
    loadBranch();
}

但是动作的主体抛出一个异常,我用普通 throw; 记录并重新抛出,但是这个 ctor 执行得很好,我的代码的其余部分继续 运行 好像什么都没有发生。这是为什么?

因为动作是异步的。 loadBranch() returns 一旦到达第一个 await,并且不可能抛出您期望的异常 - 这是 Task 中信息的一部分你忽略了(通过使用 Action 而不是 Func<Task>)。

总而言之,您刚刚编写了一个更加混淆的版本:

_branchClient.ReadOneItemAsync(AppSettings.BranchId, _initCts.Token);

.NET 构造函数本质上是同步的。他们不应该做任何你可以从使用异步代码中获益的事情 - 在构造函数(或构造函数调用的方法)中尽可能少做是个好主意。如果您需要复杂的操作、异步代码、I/O、大量 CPU 工作...请使用静态方法。由于您是 运行 异步代码,因此请将其 return 设为 Task<AppStateModel>,以适合整个异步流程。

另请注意,该异常将不会 在较旧的 .NET 运行时中被吞没。假设没有同步上下文,异常仍然在后台线程上抛出(异步操作的延续被发布)——thread-pool 线程上未处理异常的默认值曾经是 "bring down the whole application"。这会在 Task 对象被最终确定时发生,因此与所有程序逻辑分离,据您所知几乎是随机的。毕竟,您还能做什么 - 没有可以观察到异常的好地方,而且那里唯一的根基本上是 "I don't care about what happens with this task"。但是考虑到确保正确观察和处理每个异常是多么复杂,默认值更改为 "unobserved exceptions are ignored".

让我们解构这里发生的事情:您有一个指向匿名 async void 方法的 Action 委托。 async void 是什么意思?这意味着该方法实际上 returns 一个 Task 封装了异步逻辑。

意思是当你调用loadBranch时,它执行了一个异步方法。当异步方法命中 await 调用时,它会 returns 一个 Task 对象,允许您等待它、添加一个延续或其他任何东西。但是由于您没有明确的 Task 变量来捕获它,您只是让构造函数超出范围,没有任何代码处理 Task 的继续。这意味着当 Task 抛出时,ctor 已经退出。