ChatGPT解决这个技术问题 Extra ChatGPT

C#中“返回等待”的目的是什么?

有没有这样写方法的场景:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

而不是这个:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

会有意义吗?

既然可以从内部 DoAnotherThingAsync() 调用直接返回 Task<T>,为什么还要使用 return await 构造?

我在很多地方都看到了带有 return await 的代码,我想我可能遗漏了一些东西。但据我了解,在这种情况下不使用 async/await 关键字并直接返回 Task 在功能上是等效的。为什么要增加额外的 await 层的额外开销?

我认为您看到这一点的唯一原因是人们通过模仿学习,并且通常(如果他们不需要)他们使用他们能找到的最简单的解决方案。所以人们看到那个代码,使用那个代码,他们看到它有效,从现在开始,对他们来说,这是正确的方法......在这种情况下等待是没有用的
至少有一个重要区别:exception propagation
我也不明白,根本无法理解整个概念,没有任何意义。从我了解到如果一个方法有一个返回类型,它必须有一个返回关键字,这不是 C# 语言的规则吗?
@monstro OP的问题确实有return语句吗?

s
svick

当普通方法中的 returnasync 方法中的 return await 行为不同时,有一种偷偷摸摸的情况:当与 using 组合时(或者更一般地,在 try 块中的任何 return await)。

考虑这两个版本的方法:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

第一个方法将在 DoAnotherThingAsync() 方法返回后立即 Dispose() Foo 对象,这可能在它实际完成之前很久。这意味着第一个版本可能有问题(因为 Foo 处理得太快了),而第二个版本可以正常工作。


为了完整起见,在第一种情况下,您应该返回 foo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
@ghord 那行不通,Dispose() 返回 void。您需要类似 return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; }); 的内容。但是我不知道当您可以使用第二个选项时为什么要这样做。
@svick 你是对的,它应该更像{ var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }。用例非常简单:如果您使用 .NET 4.0(像大多数人一样),您仍然可以以这种方式编写异步代码,这将很好地从 4.5 应用程序调用。
@ghord 如果您使用 .Net 4.0 并且想要编写异步代码,您可能应该使用 Microsoft.Bcl.Async。并且您的代码仅在返回的 Task 完成后才处理 Foo,我不喜欢这样做,因为它不必要地引入了并发性。
@svick您的代码也会等到任务完成。此外,Microsoft.Bcl.Async 由于对 KB2468871 的依赖以及在使用 .NET 4.0 异步代码库和正确的 4.5 异步代码时发生冲突,因此对我来说无法使用。
S
Stephen Cleary

如果您不需要 async(即,您可以直接返回 Task),则不要使用 async

在某些情况下 return await 很有用,例如您有 两个 异步操作要做:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

有关 async 性能的更多信息,请参阅 Stephen Toub 关于该主题的 MSDN articlevideo

更新:我编写了一个更详细的 blog post


您能否解释一下为什么 await 在第二种情况下有用?为什么不做 return SecondAwait(intermediate);
我和马特有同样的问题,在这种情况下,return SecondAwait(intermediate); 是否也能实现目标?我认为 return await 在这里也是多余的......
@MattSmith 那不会编译。如果你想在第一行使用 await,你也必须在第二行使用它。
@svick 因为它们只是按顺序运行,所以应该将它们更改为像 var intermediate = First(); return Second(intermediate) 这样的正常调用,以避免并行引入的开销。在这种情况下不需要异步调用,是吗?
@TomLint It really doesn't compile. 假设 SecondAwait 的返回类型是 `string,错误消息是:“CS4016:由于这是一个异步方法,返回表达式必须是 'string' 类型而不是 'Task' ”。
S
Servy

您想要这样做的唯一原因是早期代码中是否存在其他 await,或者您在返回结果之前以某种方式对结果进行了操作。可能发生这种情况的另一种方式是通过更改异常处理方式的 try/catch。如果您没有这样做,那么您是对的,没有理由增加创建方法 async 的开销。


与斯蒂芬的回答一样,我不明白为什么需要 return await(而不仅仅是返回子调用的任务)即使早期代码中有其他等待。你能提供解释吗?
@TX_ 如果您想删除 async 那么您将如何等待第一个任务?如果您想使用 any 等待,则需要将该方法标记为 async。如果该方法被标记为 async 并且您在代码的前面有一个 await,那么您需要 await 第二个异步操作以使其具有正确的类型。如果您刚刚删除了 await,则它不会编译,因为返回值的类型不正确。由于方法是 async,因此结果总是包装在任务中。
@Noseratio 试试这两个。第一个编译。第二个没有。错误消息会告诉您问题所在。您不会返回正确的类型。如果在 async 方法中不返回任务,则返回任务的结果,然后将其包装。
@Servy,当然 - 你是对的。在后一种情况下,我们将显式返回 Task<Type>,而 async 指示返回 Type(编译器本身会变成 Task<Type>)。
@Itsik 好吧,async 只是用于显式连接延续的语法糖。您需要 async 做任何事情,但是在执行任何重要的异步操作时,它显着地 更容易使用。例如,您提供的代码实际上并没有像您希望的那样传播错误,并且在更复杂的情况下正确执行此操作开始变得相当困难。虽然您从不需要 async,但我描述的情况是使用它可以增加价值。
A
Andrew Arnott

您可能需要等待结果的另一种情况是:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

在这种情况下,GetIFooAsync() 必须等待 GetFooAsync 的结果,因为两种方法之间的 T 类型不同,并且 Task<Foo> 不能直接分配给 Task<IFoo>。但是,如果您等待结果,它就会变成 Foo,而 可以直接分配给 IFoo。然后 async 方法只是将结果重新打包到 Task<IFoo> 中,然后就可以走了。


同意,这真的很烦人 - 我相信根本原因是 Task<> 是不变的。
h
haimb

如果您不使用 return await 您可能会在调试时或在异常日志中打印时破坏您的堆栈跟踪。

当您返回任务时,该方法实现了它的目的,并且它不在调用堆栈中。当您使用 return await 时,您会将其留在调用堆栈中。

例如:

使用 await 时调用堆栈:A 等待 B 的任务 => B 等待 C 的任务

不使用 await 时调用堆栈:A 等待来自 C 的任务,而 B 已返回。


这是一篇很好的文章:vkontech.com/…
A
Andrew Arnott

使原本简单的“thunk”方法异步在内存中创建一个异步状态机,而非异步则没有。虽然这通常可以指出人们使用非异步版本,因为它更有效(这是真的),但这也意味着在挂起的情况下,您没有证据表明该方法涉及“返回/继续堆栈”这有时会让人更难理解这个问题。

所以是的,当性能不重要时(通常不是),我将在所有这些 thunk 方法上抛出异步,以便我有异步状态机来帮助我稍后诊断挂起,并帮助确保如果那些thunk 方法会随着时间的推移而发展,它们肯定会返回错误的任务而不是 throw。


h
heltonbiker

这也让我感到困惑,我觉得以前的答案忽略了你的实际问题:

当您可以从内部 DoAnotherThingAsync() 调用中直接返回 Task 时,为什么要使用 return await 构造?

嗯,有时您实际上想要一个 Task<SomeType>,但大多数时候您实际上想要一个 SomeType 的实例,即任务的结果。

从您的代码:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

不熟悉语法的人(比如我)可能会认为这个方法应该返回一个 Task<SomeResult>,但由于它被标记为 async,这意味着它的实际返回类型是 SomeResult。如果您只使用 return foo.DoAnotherThingAsync(),您将返回一个无法编译的任务。正确的方法是返回任务的结果,所以return await


“实际返回类型”。诶? async/await 不会改变返回类型。在您的示例中,var task = DoSomethingAsync(); 会给您一个任务,而不是 T
@Shoe 我不确定我是否理解 async/await 的事情。据我了解,Task task = DoSomethingAsync()Something something = await DoSomethingAsync() 都有效。第一个为您提供正确的任务,而第二个,由于 await 关键字,在任务完成后为您提供任务的结果。例如,我可以有 Task task = DoSomethingAsync(); Something something = await task;
S
Sedat Kapanoglu

您可能需要 return await 的另一个原因:await 语法支持 Task<T>ValueTask<T> 返回类型之间的自动转换。例如,即使 SubTask 方法返回 Task<T> 但其调用方返回 ValueTask<T>,下面的代码仍然有效。

async Task<T> SubTask()
{
...
}

async ValueTask<T> DoSomething()
{
  await UnimportantTask();
  return await SubTask();
}

如果您跳过 DoSomething() 行上的 await,您将收到编译器错误 CS0029:

无法将类型“System.Threading.Tasks.Task”隐式转换为“System.Threading.Tasks.ValueTask”。

如果您尝试显式类型转换,您将获得 CS0030。不要打扰。

顺便说一下,这是 .NET Framework。我完全可以预见到评论说“在 .NET hypothetical_version 中已修复”,我还没有测试过。 :)