为什么我需要等待已经等待的呼叫(.NET Core 2.2)

Why am I required to await a call when it's already been await'ed (.NET Core 2.2)

我有服务方法return正在等待成员视图模型。

public async Task<MemberVm> GetMember(Guid id)
{
  Task<Member> output = Context.Members
    .SingleOrDefaultAsync(e => e.Id == id);

  return await output != null
    ? new MemberVm(output)
    : null;
}

由于new MemberVm(output),这无法编译。相反,计算机要求我做 new MemberVm(await output)。如果它是一个简单的 return 语句,我会理解它,但在这种情况下,在评估条件表达式时已经等待它。对我来说,这似乎是伪代码。

if(await output != null)
  return await-again-but-why new MemberVm(output)
else
  return null;

我是不是做错了,或者这只是语言语法的意外和不幸的结果?

这个有用吗?

MemberVm 的构造函数是什么样的?

public async Task<MemberVm> GetMember(Guid id)
{
  var output = await Context.Members
    .SingleOrDefaultAsync(e => e.Id == id);

  if (output == null)
     return null;

  return new MemberVm(output);
}

MemberVm 的构造函数似乎没有在其构造函数中使用 Task 参数(尽管没有看到代码,我不能确定)。相反,我认为构造函数只需要一个常规的 MemberVm 参数,因此通过先评估 Context.Members... 调用,应该有助于解决您正在进行的问题。如果没有,请告诉我,我们会解决的。

它无法编译,因为 outputTask,而不是 Member

这可行:

public async Task<MemberVm> GetMember(Guid id)
{
  Member member = await Context.Members
    .SingleOrDefaultAsync(e => e.Id == id);

  return member != null
    ? new MemberVm(member)
    : null;
}

这不是 :

Task<Member> output = Context.Members
                             .SingleOrDefaultAsync(e => e.Id == id);

return await output != null // <= "await output" is null or a Member instance
    ? new MemberVm(output)  // "output" is always a Task<Member>
    : null;

通过写await output "ouput"本身不会被await的结果替换。它仍然是对您在上面创建的任务的相同引用。


无关:我不建议返回 null。我想我会 MemberVM 处理使用 null 设置的问题,或者如果这强烈表明应用程序代码或数据库一致性有问题则抛出异常。

如果您还没有读过 async..await 的工作原理,您可能应该更好地理解它;但是这些关键字主要做的是触发将原始代码自动重写为连续传递样式。

基本上发生的是您的原始代码被转换为:

public Task<MemberVm> GetMember(Guid id)
{
    Task<Member> output = Context.Members
        .SingleOrDefaultAsync(e => e.Id == id);
    return output.ContinueWith((Task<Member> awaitedOutput) => 
        awaitedOutput.Result != null ? new MemberVm(output.Result) : null);
}

原始 output 变量保持不变,等待(可以这么说)的结果在可用时传递到延续中。由于您没有将其保存到变量中,因此在首次使用后您将无法使用它。 (这是我调用的 lambda 参数 awaitedOutput,如果您不自己将等待的输出分配给变量,它实际上可能是 C# 编译器生成的乱码。)

在你的情况下,将等待的值存储在变量中可能是最简单的

public Task<MemberVm> GetMember(Guid id)
{
    Member output = await Context.Members
        .SingleOrDefaultAsync(e => e.Id == id);
    return output != null
        ? new MemberVm(output)
        : null;
}

您也可以直接在 await 下的代码中使用 output.Result,但这并不是您真正应该做的事情,而且它有点容易出错。 (如果您出于某种原因无意中将 output 重新分配给不同的任务。这将导致整个线程 Wait(),我猜它会冻结。)


至关重要的是,说 "a call that's already been awaited" 没有任何意义。在后台,await 不是您对电话或任务的事情;这是对编译器的指令,将 await 之后的所有代码打包到一个闭包中,将其传递给 Task.ContinueWith(),然后 立即 return 新的任务。也就是说:await 本身并不会导致等待调用结果,它会导致等待代码被注册为回调,以便在结果可用时调用。如果您 await 一个结果已经可用的 Task,所有的变化就是这个回调将被更快地调用。

实现异步的方法是,在您需要等待调用完成的每个点,控制 return 代码外的某个事件循环。这个事件循环监视东西到达 "from the outside"(例如一些 I/O 操作完成),并唤醒正在等待它的任何延续链。当您 await 同一个 Task 不止一次时,所发生的只是它处理了几个这样的回调。

(假设,是的,编译器也可以转换代码,以便在等待之后,原始变量名称引用新值。但是我认为它没有以这种方式实现的原因有很多 - 类型改变中间函数的变量在 C# 中是前所未有的,并且会令人困惑;而且通常看起来它会更复杂,更难对整体进行推理。)


在这里进行一个有希望的说明性切线:我相信当你在同一个 async 函数中 await 一个任务两次时或多或少会发生的是:

  1. 执行到达第一个 await,回调(包含已创建并传递给 ContinueWith(),控制 returns 到顶层。
  2. 一段时间后,调用的结果可用,回调最终以结果作为参数被调用。
  3. 第一个回调执行到第二个等待,创建另一个回调并传递给ContinueWith()
  4. 由于结果已经可用,因此可能会立即调用第二个回调,并且函数的其余部分 运行s.

如您所见,等待同一个任务两次基本上是在事件循环中毫无意义地绕行。如果您需要任务的值,只需将其放入一个变量即可。根据我的经验,很多时候你还不如立即 await 调用任何 async 函数,并将此语句尽可能靠近使用任务结果的位置。 (因为 await 之后的任何代码都不会 运行 直到结果可用,即使它没有使用该结果。)例外情况是如果你有一些代码,你需要在启动后 运行调用,但在使用结果之前,由于某种原因你不能在调用开始之前 运行。

了解编译器标记是类型问题这一问题很重要。 MemberVm 构造函数采用 Member 参数,但您的 output 变量的类型为 Task<Member>。编译器并不真的希望您再次等待 Task,但这是从 Task 中提取结果并使类型起作用的最常见方法。另一种重写代码的方法是更改​​ output:

的类型
Member output = await Context.Members.SingleOrDefaultAsync(e => e.Id == id);

现在您可以将 output 直接传递给 MemberVm 构造函数,因为您已经保存了第一个 await 的结果。

此处已经有正确答案,但解释远比必要的复杂。 await 关键字都保持执行直到任务完成 展开任务(即 Task<Member> 变成 Member)。但是,您没有坚持那个展开的部分。

第二个output还是一个Task<Member>。现在已经完成了,但是还没有解包,因为你没有保存解包的结果。