从 C# 异步方法返回 Void
不是一个微不足道的话题
正如本系列的上一篇指南中提到的, C# 中的异步方法通常被称为async Task或async void,引用两种主要返回类型。通常,您会希望通过返回Task来坚持使用前者,但返回void的异步方法也很重要。然而,可能并不总是容易确定何时合适。幸运的是,有一些指南可以帮助做出这个决定。
何时会考虑归还虚空?
很难过分强调这一事实:在决定异步方法的返回类型时,绝大多数情况下返回Task是正确的选择。但除非应用程序的Main入口点本身是异步的(从 C# 7.1 开始支持),否则在某些时候您将需要一个返回void的异步方法。这在一定程度上是由于返回Task的异步方法具有“传染性”,因此它们的调用方法通常也必须变为async。因此,从调用方法返回void可以成为一种隔离传染的方法。
然而,这其中存在着危险。假设您有一个现有的同步方法,该方法在应用程序的许多不同位置被调用,当您修改该方法时,您发现需要异步等待某些操作。因此,您添加了await调用并将async Task应用于方法签名,但随后发现现在在调用该方法的每个位置都有编译错误。由于不愿意检查和修改每个调用方法,您尝试将async Task更改为async void并重新编译。假设这次应用程序编译成功。很棒,对吧?
嗯,可能不是。从异步方法返回void有很多正当理由,但“它更容易”和“它让我的应用程序可以编译”不在该列表中。请记住,更改返回类型会对控制流产生影响。事实证明,从异步方法返回void 的正当理由范围非常有限。
退货的正当理由无效
无需多言,以下是从异步方法返回void 的正当理由:
- 该方法是一个事件处理程序。事件处理程序是一种在发生某些预期的“事情”或事件时调用的方法。这可能是系统生成的事件,例如系统规定的某个截止日期到期,也可能是用户生成的事件,例如用户单击按钮。
- 该方法是一个“命令”,通常意味着它是ICommand.Execute的实现。ICommand接口是一些 C# 开发人员用来在“视图模型”级别表示事件的模式的一部分,因此其Execute方法在逻辑上与事件处理程序相同。
除非您使用称为 MVVM 的架构模式,否则您可能不会遇到命令。
- 该方法是一个回调。回调是一种在完成某些异步操作后调用的方法。理论上,使用async/await的 C# 开发人员不需要使用或实现回调,但有时您可能需要使用不使用async/await的库,而是要求您提供回调方法。除非库需要异步回调,否则您需要提供一个具有同步签名的回调,对于异步方法,这只能通过返回void来实现。
简而言之,如果你的异步方法是事件处理程序或回调,那么返回void是可以的。它们的共同点是,它们是对某种信号的反应。
未等待事件处理程序
为了更好地理解为什么事件处理程序(或任何类型的信号反应)属于自己的类别,想象一下带有按钮的应用程序。假设按钮启动了一个创建某些东西的过程;也许它使用一种算法来生成带有随机输入的分形艺术图像。这在计算上很昂贵,因此在速度较慢的设备上可能需要一些时间。作为应用程序开发人员,您可能希望允许用户通过连续多次单击按钮来同时生成多个图像。单击按钮只会发送一个表示“生成图像”的信号;它不需要等待第一幅图像生成后再发出需要第二幅图像的信号。
那么,自然就不会等待事件处理程序;信号只是被发送,而事件处理程序或回调方法将成为起点。另一种看待它的方式是,事件处理程序方法通常不会被显式调用,如果被调用,方法的调用者对结果也不感兴趣。由于从异步方法返回Task仅在以某种方式引用Task时才有用(通常通过await隐式引用),因此从事件处理程序或回调方法返回Task毫无用处。出于这个原因,并且作为一般惯例,让异步事件处理程序和回调方法返回void是合适的。
避免将 Void 返回“发射后不管”
您可能会发现,有时需要定义一个方法,它不是事件处理程序或回调,但与方法的调用者无需关心结果类似。例如,您可能希望将 API 调用发布到收集分析数据的远程服务器。您不需要进度条,用户也不需要知道它何时完成,甚至不知道它是否失败。这种事情可以称为“触发后不管”,有时开发人员会为此使用返回void的异步方法。继续我们之前的示例,假设“生成”按钮单击也应尝试将该单击记录到远程服务器。
void Initialize()
{
generateButton.OnClicked += OnGenerateButtonClicked;
}
void OnGenerateButtonClicked()
{
PostAnalyticAction("generate button clicked");
GenerateImage();
}
async void PostAnalyticAction(string actionName)
{
await new HttpClient().PostAsync("https://...", new StringContent(actionName));
}
在上面的例子中,PostAnalyticAction是“触发后不管”方法。故意不等待此方法,以便在PostAnalyticAction完成之前继续执行OnGenerateButtonClicked。这允许在收到来自远程分析服务器的响应之前开始生成图像。
虽然这种“发射后不管”的方法在许多情况下可能效果很好,但不建议这样做。请记住,大多数情况下,正确的选择是从异步方法返回Task,除非它是事件处理程序或回调。在此示例中,PostAnalyticAction既不是,也不是,并且是显式调用而不是隐式调用,这表明调用者(OnGenerateButtonClicked)可能对结果感兴趣(包括其中可能出现的任何异常)。
更好的方法是从PostAnalyticAction返回Task并在OnGenerateButtonClicked中等待它。这将要求将OnGenerateButtonClicked标记为async。
“但是等等!这是否意味着图像生成将被延迟,直到收到来自远程分析服务器的响应?”
如果您有这个想法,那就太好了!您说得对。我们现在不再“忘记”我们的方法;我们正在等待它完成。我们可以通过将分析调用作为我们做的最后一件事来“修复”这个问题。如果 await 之后没有代码,则等待时间不会延迟任何事情。以下是更新后的代码示例:
async void OnGenerateButtonClicked()
{
GenerateImage();
await PostAnalyticAction("generate button clicked");
}
async Task PostAnalyticAction(string actionName)
{
await new HttpClient().PostAsync("https://...", new StringContent(actionName));
}
请注意,现在我们拥有的唯一异步 void方法是OnGenerateButtonClicked,它是一个事件处理程序。通常你会发现,通过深思熟虑的重构,你可以避免“发射后不管”的需要。如果重新排列代码不起作用,请考虑采用松散耦合的方法,这样你就不会直接调用相关方法。例如,你可以使用某种发布/订阅消息,甚至可以使用线程安全的集合,你可以同步添加到集合中,并使用单独的线程监视新项目。不过,你的第一选择应该始终是将Task与await结合使用。
尤其不鼓励在返回Task的异步方法中以“发射后不管”的方式调用方法,因为返回Task的含义是它包含其所有行为,包括可能生成的任何其他子Task实例。
如果你真的需要“发射后不管”
有时将现有方法切换到异步任务可能会对您的代码库造成极大的侵入。在这种情况下,“发射后不管”方法可能是有效的,至少可以作为临时措施。如果您确实需要使用“发射后不管”,请确保在async void方法中添加带有某种日志记录的异常处理程序。这将帮助您避免崩溃,同时确保您意识到否则可能未被发现的问题。使用我们原来的“发射后不管”示例作为起点,它可能很简单,如下所示:
async void PostAnalyticAction(string actionName)
{
try
{
await new HttpClient().PostAsync("https://...", new StringContent(actionName));
}
catch (Exception ex)
{
Console.WriteLine($"Exception while posting analytics: {ex}");
}
}
您可能还会看到术语“触发并忘记”,指的是一种类似的方法,即异步方法返回Task,但调用者并不等待该Task。同样不鼓励使用这种方法,但如果您确实要使用这种方法,则不会使用上述try / catch ;异常的记录可以通过在调用者中向Task添加延续来完成,如此处所述。
综上所述
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~