如何(以及为什么)可以避免在这些异步方法上返回一个空
本文关键字:返回 一个 异步方法 为什么 可以避免 如何 | 更新日期: 2023-09-27 18:11:04
编辑:因此,似乎有方法返回void而不是任务意味着异常在错误的(意外的?)上下文上传播。然而,我的IDE (Xamarin)仍然在我的构造函数中调用attemptdatabselload ()
"该语句未被等待,当前方法正在执行在呼叫完成之前继续。考虑使用'await'操作符或调用'等待'方法"
它为什么这么大惊小怪?当然,使用异步方法的全部目的正是为了使程序在主线程上继续执行。
我已经读了相当多的异步和等待,因为我需要有一些异步数据加载的应用程序,我正在制作。我已经在很多地方读到,这是不好的做法,有一个异步方法返回void(除了在触发事件的情况下),我理解为什么它可以很好地保持对任务的处理。然而,我看不出我下面写的东西有什么逻辑上的错误,所以我的问题是双重的:为什么我现在的代码是糟糕的实践?它应该如何重写?
private const int MAX_CONNECTION_ATTEMPTS = 10;
private int ConnectionAttempts = 0;
//Constructor
public DataLoader()
{
//First load up current data from local sqlite db
LoadFromLocal();
//Then go for an async load from
AttemptDatabaseLoad();
}
public async void AttemptDatabaseLoad()
{
while(ConnectionAttempts < MAX_CONNECTION_ATTEMPTS){
Task<bool> Attempt = TryLoad ();
bool success = await Attempt;
if (success) {
//call func to load data into program memory proper
}else{
ConnectionAttempts++;
}
}
}
//placeholder for now
public async Task<bool> TryLoad()
{
await Task.Delay(5000);
return false;
}
构造函数用于在初始化后将对象带入其完全构造的结构。另一方面,异步方法和构造函数不能很好地配合使用,因为构造函数本质上是同步的。
解决这个问题的方法通常是为类型公开一个初始化方法,该方法本身是async的。现在,让调用者完全初始化对象。注意,这将要求您监视方法的实际初始化。Async在你需要规模的时候很好用。如果您不希望这成为应用程序中的IO瓶颈,可以考虑使用同步方法。这样做的好处是,一旦构造函数完成执行,就可以完全初始化对象。尽管如此,我不认为我会通过构造函数发起对数据库的调用:
public async Task InitializeAsync()
{
LoadFromLocal();
await AttemptDatabaseLoadAsync();
}
public async Task AttemptDatabaseLoadAsyncAsync()
{
while(ConnectionAttempts < MAX_CONNECTION_ATTEMPTS)
{
Task<bool> Attempt = TryLoad ();
bool success = await Attempt;
if (success)
{
//call func to load data into program memory proper
}
else
{
ConnectionAttempts++;
}
}
}
并命名为:
var dataLoader = new DataLoader();
await dataLoader.InitializeAsync();
我明白为什么保持Task的句柄是好的。
因此,似乎让方法返回void而不是task意味着异常在错误的(意外的?)上下文中传播。
使用Task
的原因之一是您可以使用它来检索异步方法的结果。这里的"结果"并不仅仅指返回值——我也指异常。 Task
表示异步方法的执行。
当异常转义async Task
方法时,它被放置在返回的任务上。当异常转义async void
方法时,它没有明显的去处,因此实际的行为是在async void
方法开始时当前的SynchronizationContext
上直接引发它。这听起来很奇怪,但它是专门设计来模拟异常转义事件处理程序的。
async void
方法不是事件处理程序(如本例),那么行为看起来非常奇怪和令人惊讶。它为什么这么大惊小怪?当然,使用异步方法的全部目的正是为了使程序在主线程上继续执行。
我想你误解了警告信息。由于Task
表示该方法的执行,因此忽略它在99.9%的情况下是错误的。通过忽略它,您的代码表示它不关心何时 async方法完成,它的返回值是什么(如果有的话),以及它是否抛出异常。很少有代码不关心这些中的任何。
应该如何重写?
我有一篇关于如何做"异步构造函数"的博文。我最喜欢的方法是异步工厂方法:
//Constructor
private DataLoader()
{
//First load up current data from local sqlite db
LoadFromLocal();
}
public static async Task<DataLoader> CreateAsync()
{
var result = new DataLoader();
await result.AttemptDatabaseLoadAsync();
return result;
}
然而,由于您是在UI应用程序中使用它,我怀疑您最终会遇到想要从ViewModel构造函数调用异步代码的情况。异步工厂对于辅助代码(如DataLoader
)来说是很好的,但是对于ViewModels来说却行不通,因为vm需要立即创建—UI需要现在显示。
在UI层,你必须首先将你的UI初始化为某种"加载"状态,然后将更新为"正常"状态。一旦数据到达,就声明。我更喜欢使用异步数据绑定,正如我在MSDN文章中所描述的那样。
您可以将返回类型更改为Task(非泛型),并且不从async方法返回"显式"。为什么最好只在顶级函数上使用void的原因可以在这里找到:async/await -何时返回Task vs void?它主要是关于在async-void方法中从异常中恢复。我希望它会有所帮助。
编辑:还有一件事-因为我没有注意到你是从构造函数调用它。也请查看这个答案:https://stackoverflow.com/a/23051370/580207和这个博客文章:http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html
为什么我现在的代码很差?
DataLoader()
构造函数的调用者可能遇到以下问题:
-
实例化
DataLoader
类的代码不知道DataLoader()
返回后加载操作仍在进行中,因此它不能使用异步AttemptDatabaseLoad()
检索到的数据。 -
无法发现加载的数据何时可用。
- 不能组成更大的异步操作
建议的更改是将异步方法返回的任务存储在属性中,以便调用者可以使用它等待加载完成,或者将其组合到异步方法中。
class DataLoader
{
public DataLoader ()
{
//First load up current data from local sqlite db
LoadFromLocal();
//Then go for an async load from
this.Completion = AttemptDatabaseLoadAsync();
}
async Task AttemptDatabaseLoadAsync()
{
while(ConnectionAttempts < MAX_CONNECTION_ATTEMPTS){
Task<bool> Attempt = TryLoad ();
bool success = await Attempt;
if (success) {
//call func to load data into program memory proper
}else{
ConnectionAttempts++;
}
}
}
public Task Completion
{
get; private set;
}
}
用法:
var loader = new DataLoader();
loader.Completion.Wait();
或:
async Task SomeMethodAsync()
{
var loader = new DataLoader();
await loader.Completion;
}