如何使用async-await模式初始化对象
本文关键字:初始化 对象 模式 async-await 何使用 | 更新日期: 2023-09-27 18:08:22
我试图在我的服务类中遵循RAII模式,这意味着当一个对象被构造时,它被完全初始化。然而,我在异步api方面遇到了困难。所讨论的类的结构如下
class ServiceProvider : IServiceProvider // Is only used through this interface
{
public int ImportantValue { get; set; }
public event EventHandler ImportantValueUpdated;
public ServiceProvider(IDependency1 dep1, IDependency2 dep2)
{
// IDependency1 provide an input value to calculate ImportantValue
// IDependency2 provide an async algorithm to calculate ImportantValue
}
}
我还致力于消除ImportantValue
getter中的副作用,使其线程安全。
现在ServiceProvider
的用户将创建它的实例,订阅ImportantValue
变化的事件,并获得初始的ImportantValue
。问题来了,关于初值。由于ImportantValue
是异步计算的,类不能在构造函数中完全初始化。最初将这个值设为null可能是可以的,但之后我需要在某个地方第一次计算它。一个自然的地方可能是ImportantValue
的getter,但我的目标是使它线程安全,没有副作用。
所以我基本上被这些矛盾困住了。你能帮助我并提供一些替代方案吗?在构造函数中初始化值虽然很好,但没有副作用,并且属性的线程安全性是强制性的。
提前感谢。
EDIT:还有一件事要添加。我正在使用Ninject进行实例化,据我所知,它不支持异步方法来创建绑定。虽然在构造函数中启动一些基于任务的操作的方法可以工作,但我不能等待它的结果。
。接下来的两种方法(到目前为止作为答案提供)将无法编译,因为返回的是Task,而不是my object:
Kernel.Bind<IServiceProvider>().ToMethod(async ctx => await ServiceProvider.CreateAsync())
或
Kernel.Bind<IServiceProvider>().ToMethod(async ctx =>
{
var sp = new ServiceProvider();
await sp.InitializeAsync();
})
简单的绑定可以工作,但我不等待在构造函数中开始的异步初始化的结果,正如Stephen Cleary所建议的:
Kernel.Bind<IServiceProvider>().To<ServiceProvider>();
…这对我来说可不太好。
我有一篇博客文章描述了几种构建async
的方法。
我推荐Reed描述的异步工厂方法,但有时这是不可能的(例如,依赖注入)。在这些情况下,您可以使用如下的异步初始化模式:
public sealed class MyType
{
public MyType()
{
Initialization = InitializeAsync();
}
public Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// Asynchronously initialize this instance.
await Task.Delay(100);
}
}
然后可以正常构造该类型,但请记住,只有构造才会启动异步初始化。当需要初始化类型时,代码可以这样做:
await myTypeInstance.Initialization;
注意,如果Initialization
已经完成,执行(同步)将继续超过await
。
如果你想要一个真正的异步属性,我也有一篇博文。您的情况听起来可能会受益于AsyncLazy<T>
:
public sealed class MyClass
{
public MyClass()
{
MyProperty = new AsyncLazy<int>(async () =>
{
await Task.Delay(100);
return 13;
});
}
public AsyncLazy<int> MyProperty { get; private set; }
}
一个可能的选择是将其移到工厂方法中,而不是使用构造函数。
您的工厂方法可以返回一个Task<ServiceProvider>
,这将允许您异步执行初始化,但在(异步)计算ImportantValue
之前不会返回构造的ServiceProvider
。
这将允许您的用户编写如下代码:
var sp = await ServiceProvider.CreateAsync();
int iv = sp.ImportantValue; // Will be initialized at this point
这是对@StephenCleary异步初始化模式的一个轻微修改。
不同之处在于调用者不需要"记住"await
和InitializationTask
,甚至不需要知道initializationTask
的任何信息(实际上它现在被更改为private)。
它的工作方式是在每个使用初始化数据的方法中都有对await _initializationTask
的初始调用。这将立即返回第二次-因为_initializationTask
对象本身将有一个布尔集合(IsCompleted
由'await'机制检查)-所以不用担心它初始化多次。
我所知道的唯一问题是你不能忘记在使用数据的每个方法中调用它。
public sealed class MyType
{
public MyType()
{
_initializationTask = InitializeAsync();
}
private Task _initializationTask;
private async Task InitializeAsync()
{
// Asynchronously initialize this instance.
_customers = await LoadCustomersAsync();
}
public async Task<Customer> LookupCustomer(string name)
{
// Waits to ensure the class has been initialized properly
// The task will only ever run once, triggered initially by the constructor
// If the task failed this will raise an exception
// Note: there are no () since this is not a method call
await _initializationTask;
return _customers[name];
}
// one way of clearing the cache
public void ClearCache()
{
InitializeAsync();
}
// another approach to clearing the cache, will wait until complete
// I don't really see a benefit to this method since any call using the
// data (like LookupCustomer) will await the initialization anyway
public async Task ClearCache2()
{
await InitializeAsync();
}
}
您可以使用我的AsyncContainer IoC容器,它支持与您完全相同的场景。
它还支持其他方便的场景,如异步初始化器,运行时条件工厂,依赖于异步和同步工厂函数
//The email service factory is an async method
public static async Task<EmailService> EmailServiceFactory()
{
await Task.Delay(1000);
return new EmailService();
}
class Service
{
//Constructor dependencies will be solved asynchronously:
public Service(IEmailService email)
{
}
}
var container = new Container();
//Register an async factory:
container.Register<IEmailService>(EmailServiceFactory);
//Asynchronous GetInstance:
var service = await container.GetInstanceAsync<Service>();
//Safe synchronous, will fail if the solving path is not fully synchronous:
var service = container.GetInstance<Service>();
我知道这是一个古老的问题,但它是第一个出现在谷歌上,坦率地说,公认的答案是一个糟糕的答案。永远不要为了使用await操作符而强制延迟。
初始化方法的更好方法:
private async Task<bool> InitializeAsync()
{
try{
// Initialize this instance.
}
catch{
// Handle issues
return await Task.FromResult(false);
}
return await Task.FromResult(true);
}
这将使用异步框架来初始化你的对象,但随后它将返回一个布尔值。
为什么这是一个更好的方法?首先,你没有强迫代码延迟,我认为这完全违背了使用异步框架的目的。其次,从异步方法返回一些东西是一条很好的经验法则。这样,您就知道您的async方法是否实际工作/做了它应该做的事情。只返回Task就相当于在非异步方法上返回void。
我有一个Stephen Cleary的异步初始化模式示例的变体。您可以封装Initialization属性并在类方法中等待它。在这种情况下,客户端代码将不需要等待初始化任务。
public class ClassWithAsyncInit
{
public ClassWithAsyncInit()
{
Initialization = InitializeAsync();
}
private Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// your async init code
}
public async Task FirstMethod()
{
await Initialization;
// ... other code
}
}
缺点是,如果您的类中有很多方法,并且需要等待每个方法中的Initialization任务,则不方便。但有时也没关系。假设您有一个用于保存JSON对象的简单接口:
public IDataSaver
{
void Save(string json);
}
您需要使用异步初始化逻辑为数据库实现它。考虑到您将只有一个公共方法,封装Initialization属性并在Save方法中等待它是有意义的:
public class SomeDbDataSaver: IDataSaver
{
protected DatabaseClient DbClient { get; set; }
public SomeDbDataSaver()
{
DbClient = new DatabaseClient();
Initialization = InitializeAsync(); // start off the async init
}
private Task Initialization { get; private set; }
private async Task InitializeAsync()
{
await DbClient.CreateDatabaseIfNotExistsAsync("DatabaseName");
}
public async Task Save(string json)
{
await Initialization;
// ... code for saving a data item to the database
}
}