异步构造提出了一个有趣的问题。能够await
在构造函数中使用会很有用,但这意味着构造函数必须返回一个Task
表示将来构造的值,而不是构造值。这种概念很难融入现有的语言。
底线是async
不允许使用构造函数,所以让我们探索一些替代方案。
构造函数不能async
,但静态方法可以。拥有静态创建方法非常容易,使类型成为自己的工厂:
public sealed class MyClass
{
private MyData asyncData;
private MyClass() { ... }
private async Task InitializeAsync()
{
asyncData = await GetDataAsync();
return this;
}
public static Task CreateAsync()
{
var ret = new MyClass();
return ret.InitializeAsync();
}
}
public static async Task UseMyClassAsync()
{
MyClass instance = await MyClass.CreateAsync();
...
}
可以Create
做所有的初始化工作,但我更喜欢这个async InitializeAsync
方法。
工厂方法是最常见的异步构造方法,但在某些情况下还有其他有用的方法。
AsyncLazy(用于资源)如果您正在创建的实例是共享资源,那么您可以使用异步延迟初始化来创建您的共享实例:
private static AsyncLazy resource = new AsyncLazy(async () =>
{
var data = await GetResource();
return new MyResource(data);
});
public static async Task UseResourceAsync()
{
MyResource res = await resource;
}
AsyncLazy
非常适合资源;在此示例中,resource
将在第一次await
编辑时开始构建。await
它将绑定到同一构造中的任何其他方法,并且当构造完成时,所有服务员都被释放。构造完成后的任何await
s 都会立即继续,因为该值已经可用。
如果实例不用作共享资源,则此方法效果不佳。如果实例不是共享资源,则应使用另一种方法。
异步初始化模式已经介绍了异步构造的最佳方法:异步工厂方法和AsyncLazy
. 这些是最好的方法,因为您永远不会公开未初始化的实例。
但是,有时您确实需要构造函数,例如,当某些其他组件使用反射来创建您的类型的实例时。这包括数据绑定、IoC 和 DI 框架Activator.CreateInstance
等。
在这些情况下,您必须返回一个未初始化的实例,但您可以通过应用一个通用模式来缓解这种情况:每个需要异步初始化的对象都将公开一个Task Initialization { get; }
包含异步初始化结果的属性。
如果您想将异步初始化视为实现细节,您可以(可选地)为使用异步初始化的类型定义一个“标记”接口:
///
/// Marks a type as requiring asynchronous initialization and provides the result of that initialization.
///
public interface IAsyncInitialization
{
///
/// The result of the asynchronous initialization of this instance.
///
Task Initialization { get; }
}
异步初始化的模式如下所示:
public sealed class MyFundamentalType : IAsyncInitialization
{
public MyFundamentalType()
{
Initialization = InitializeAsync();
}
public Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// Asynchronously initialize this instance.
await Task.Delay(100);
}
}
这种模式非常简单,但它为我们提供了一些重要的语义:
- 初始化在构造函数中开始(当我们调用 时
InitializeAsync
)。 - 初始化的完成被暴露(通过
Initialization
属性)。 - 异步初始化引发的任何异常都将被捕获并放置在
Initialization
属性上。
这种类型的实例可以(手动)像这样构造:
var myInstance = new MyFundamentalType();
// Danger: the instance is not initialized here!
await myInstance.Initialization;
// OK: the instance is initialized now.
使用异步初始化组合
很容易创建依赖于这种基本类型的另一种类型(即异步组合):
public sealed class MyComposedType : IAsyncInitialization
{
private readonly MyFundamentalType _fundamental;
public MyComposedType(MyFundamentalType fundamental)
{
_fundamental = fundamental;
Initialization = InitializeAsync();
}
public Task Initialization { get; private set; }
private async Task InitializeAsync()
{
// Asynchronously wait for the fundamental instance to initialize.
await _fundamental.Initialization;
// Do our own initialization (synchronous or asynchronous).
await Task.Delay(100);
}
}
主要区别在于我们在继续初始化之前等待所有组件初始化。或者,您可以继续进行一些初始化,仅在需要完成特定组件时才等待特定组件。但是,每个组件都应该在InitializeAsync
.
在编写时,我们从这种模式中获得了一些关键语义:
- 在所有组件的初始化完成之前,组合类型的初始化不会完成。
- 来自组件初始化的任何错误都会通过组合类型浮出水面。
- 组合类型支持异步初始化,并且可以像任何其他支持异步初始化的类型一样依次组合。
此外,如果您正在使用IAsyncInitialization
“标记”接口,您可以对其进行测试并异步初始化 IoC/DI 提供给您的实例。这会使您稍微复杂化InitializeAsync
,但允许您将异步初始化视为实现细节。例如,如果_fundamental
是类型IMyFundamentalType
:
private async Task InitializeAsync()
{
// Asynchronously wait for the fundamental instance to initialize if necessary.
var asyncFundamental = _fundamental as IAsyncInitialization;
if (asyncFundamental != null)
await asyncFundamental.Initialization;
// Do our own initialization (synchronous or asynchronous).
await Task.Delay(100);
}
顶级处理
我们已经介绍了如何使用异步初始化编写“基本”类型,以及如何使用异步初始化将它们“组合”成其他类型。最终,您将需要使用支持异步初始化的高级类型。
在许多动态创建场景(例如 IoC/DI/ Activator.CreateInstance
)中,您可以直接检查IAsyncInitialization
并初始化它:
object myInstance = ...;
var asyncInstance = myInstance as IAsyncInitialization;
if (asyncInstance != null)
await asyncInstance.Initialization;
但是,如果您通过数据绑定创建类型,或者使用 IoC/DI 将视图模型注入到视图的数据上下文中,那么您实际上并没有与顶级实例交互的地方。除非初始化失败,否则数据绑定将在初始化完成时更新 UI ,因此您需要显示失败。不幸的是,Task
没有实现INotifyPropertyChanged
,所以任务完成不会自动浮出水面。您可以在 AsyncEx 库中使用类似NotifyTaskCompletion 类型的类型来简化此操作:
public sealed class MyViewModel : INotifyPropertyChanged, IAsyncInitialization
{
public MyViewModel()
{
InitializationNotifier = NotifyTaskCompletion.Create(InitializeAsync());
}
public INotifyTaskCompletion InitializationNotifier { get; private set; }
public Task Initialization { get { return InitializationNotifier.Task; } }
private async Task InitializeAsync()
{
await Task.Delay(100); // asynchronous initialization
}
}
您的数据绑定代码可以使用类似InitializationNotifier.IsCompleted
和的路径InitializationNotifier.ErrorMessage
来响应初始化任务的完成。
我确实更喜欢异步工厂方法而不是异步初始化模式。异步初始化模式确实会在初始化之前公开实例,并且依赖于程序员正确使用Initialization
. 但是在某些情况下你不能使用异步工厂方法,异步初始化是一个不错的解决方法。
下面是一个不该做什么的例子:
public sealed class MyClass
{
private MyData asyncData;
public MyClass()
{
InitializeAsync();
}
// BAD CODE!!
private async void InitializeAsync()
{
asyncData = await GetDataAsync();
}
}
乍一看,这似乎是一种合理的方法:您会得到一个启动异步操作的常规构造函数;但是,由于使用async void
.
第一个问题是,当构造函数完成时,实例仍在异步初始化中,没有明显的方法可以确定异步初始化何时完成。
第二个问题是错误处理:任何引发的异常InitializeAsync
都将直接在SynchronizationContext
构造实例时抛出。该异常不会被catch
围绕对象构造的任何子句捕获。大多数应用程序将此视为致命错误。
这篇文章中的前两个解决方案(异步工厂方法和AsyncLazy
)没有这些问题。它们在异步初始化之前不提供实例,并且异常处理更自然。第三种解决方案(异步初始化)确实在初始化之前返回了一个实例(我不喜欢),但它通过提供一种标准方法来检测初始化何时完成以及合理的异常处理来缓解这种情况。