我再说一遍:async await很棒。与每个伟大的工具一样,我们有责任了解如何最好地使用它。
异步传播永久链接让我们举一个 Xamarin 应用程序的示例,用户通过按下应用程序中的按钮来订购咖啡。
我们显然有一个班级负责准备咖啡:
public class CoffeeService
{
public async Task PrepareCoffeeAsync()
{
// Asynchronously prepare an awesome coffee
await Task.Delay(2000);
}
}
由于准备咖啡需要时间,而且我们现在不知道需要多长时间(因为可能发生的事件、罢工或需要找到一袋新的咖啡粒)这个方法被标记为异步并返回一个任务。
任何调用的代码PrepareCoffeeAsync
都需要等待它的完成。
最明显的方法是将调用者标记为异步,如下面的视图模型:
public class CoffeeViewModel
{
public async Task PrepareCoffeeAsync()
{
IsBusy = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
IsBusy = false;
}
}
重复相同的过程,您将达到无法将返回类型更改为 Task 的地步,您将面临异步 void。
async void 有那么糟糕吗?永久链接我可以这样总结:
- 它生成编译器警告
- 如果那里没有捕获异常,则您的应用程序已死
- 您可能没有合适的调用堆栈来调试
- 如果您的应用程序崩溃:
- 你的用户不会高兴
- 你的老板不会高兴的
- 你会失去你的工作,然后你的另一半……
实际上我不知道最后一个,但无论如何, async void 是一个坏人。
有很多很棒的文章,比如Stephen Cleary和Phil Haack的文章(下面的链接),它们详细解释了 async void 的影响,我非常鼓励你阅读它们。
但我别无选择!永久链接不幸的是,这是真的。在异步传播的某一时刻,您将到达一种无法更改返回类型的方法,例如:
- 生命周期方法
- 事件处理程序
- 代表
- Lambda 表达式
我们最终得到这样的代码:
public async void OnPrepareButtonClick(object sender, EventArgs e)
{
Button button = (Button)sender;
button.IsEnabled = false;
activityIndicator.IsRunning = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
activityIndicator.IsRunning = false;
button.IsEnabled = true;
}
删除异步无效永久链接
如果您只是对在项目中使用的可重用代码感兴趣,请查看受本文内容启发的AsyncAwaitBestPratices库。
研究代码永久链接通过一些重构,我们可以在代码中隔离异步 void方法。
但首先让我们研究和注释代码中的内容:
- 铸造成一个按钮
Button button = (Button)sender;
- 向用户展示我们正在启动一个异步操作
button.IsEnabled = false; activityIndicator.IsRunning = true;
- 运行异步代码
var coffeeService = new CoffeeService(); await coffeeService.PrepareCoffeeAsync();
- 向用户展示我们完成了
activityIndicator.IsRunning = false; button.IsEnabled = true;
在这种情况下,我们可以将步骤 2 到 4 移到异步方法中。
public async void OnPrepareButtonClick(object sender, EventArgs e)
{
Button button = (Button)sender;
await PrepareCoffeeAsync(button);
}
public async Task PrepareCoffeeAsync(Button button)
{
button.IsEnabled = false;
activityIndicator.IsRunning = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
activityIndicator.IsRunning = false;
button.IsEnabled = true;
}
现在,我们的事件处理程序在方法结束时只有一个 await 。这就是我们需要继续做的事情,并使我们的代码更安全。
删除异步无效永久链接对于事件处理程序,等待PrepareCoffeeAsync
现在是无用的。由于后面没有代码,所以不需要完成信息或结果。
它现在是典型的“一劳永逸”方法。
因此,我们可以删除异步:
public void OnPrepareButtonClick(object sender, EventArgs e)
{
Button button = (Button)sender;
PrepareCoffeeAsync(button);
}
我们不再有async void但我们还没有完成,因为没有处理任何异常!
处理异常永久链接使用 try catch 块永久链接
使用 try catch 块来处理异常当然是可能的:
public async Task PrepareCoffeeAsync(Button button)
{
try
{
button.IsEnabled = false;
activityIndicator.IsRunning = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
activityIndicator.IsRunning = false;
button.IsEnabled = true;
}
catch (Exception ex)
{
// Do something
System.Diagnostics.Debug.WriteLine(ex);
}
}
这种方法的问题是它会产生大量的代码重复,因为在典型的代码库中有很多地方存在async void 。
带扩展永久链接
我们现在需要的是一些任务的扩展方法来处理异常,这些异常可以替换整个代码中的所有这些 try catch 块。
有很多方法可以做到这一点,没有一种方法真的比其他方法更好。这总是一个品味问题。
介绍 FireAndForgetSafeAsync 和 IErrorHandler永久链接
所以让我介绍一下我最喜欢的扩展方法:
public static class TaskUtilities
{
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
public static async void FireAndForgetSafeAsync(this Task task, IErrorHandler handler = null)
#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void
{
try
{
await task;
}
catch (Exception ex)
{
handler?.HandleError(ex);
}
}
}
该FireAndForgetSafeAsync
方法基本上将任务包装到 try catch 块中。如果发生错误,它将异常发送到错误处理程序,并应实现以下接口:
public interface IErrorHandler
{
void HandleError(Exception ex);
}
当然,将错误处理程序设置为null不是什么事情!
我通常喜欢我的视图模型:
IErrorHandler
在运行时对注入有一个引用- 直接实现接口
完成代码永久链接
现在我们已经有了前两种方法,我们可以使我们的代码更安全:
public void OnPrepareButtonClick(object sender, EventArgs e)
{
IErrorHandler errorHandler = null; // Get an instance from somewhere
Button button = (Button)sender;
PrepareCoffeeAsync(button).FireAndForgetSafeAsync(errorHandler);
}
结论永久链接
通过非常复杂的代码库应用完全相同的步骤,使我和我的团队能够克服Xamarin 应用程序中的许多问题,其中异步等待的使用非常广泛。
一点一点地,四处移动和使用FireAndForgetSafeAsync
减少了崩溃的数量,并提高了我们的错误报告准确性。
最后,重要的是要记住,无论你使用什么手段,
只需删除 async void !
一如既往,请随时阅读我以前的帖子并在下面发表评论,我将非常乐意回答。