【C#TAP 异步编程】构造函数 OOP
阅读原文时间:2023年07月13日阅读:2

原文:异步 OOP 2:构造函数 (stephencleary.com)

异步构造带来了一个有趣的问题。能够在构造函数中使用会很有用,但这意味着构造函数必须返回一个表示将来将构造的值,而不是构造的值。这种概念很难融入现有的语言。await``Task<T>

底线是不允许构造函数,因此让我们探索一些替代方案。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

工厂方法是最常见的异步构造方法,但在某些情况下还有其他方法很有用。

如果要创建的实例是共享资源,则可以使用异步延迟初始化来创建共享实例:

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<T>非常适合资源;在此示例中,将在第一次编辑时开始构造。任何其他方法,它将绑定到相同的结构,并且当构造完成时,所有服务员都将被释放。施工完成后的任何 s 都会立即继续,因为该值已经可用。resource``await``await``await

如果不将实例用作共享资源,则此方法不起作用。如果实例不是共享资源,则应改用另一种方法。

异步构造的最佳方法已经介绍过了:异步工厂方法和 。这些是最好的方法,因为您永远不会公开未初始化的实例。AsyncLazy<T>

但是,有时确实需要构造函数,例如,当其他组件使用反射来创建类型的实例时。这包括数据绑定、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.

使用异步初始化进行组合

很容易创建另一个依赖于此基本类型的类型(即异步组合):

ublic 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

在撰写时,我们从此模式中获得了一些关键语义:

  • 在组合类型的所有组件的初始化完成之前,其初始化不会完成。
  • 组件初始化产生的任何错误都会通过组合类型显示出来。
  • 组合类型支持异步初始化,并且可以像任何其他支持异步初始化的类型一样依次进行组合。

此外,如果您使用的是"标记"接口,则可以对其进行测试,并异步初始化 IoC/DI 提供给您的实例。这会稍微复杂化您的身份,但允许您将异步初始化视为实现细节。例如,如果 是 的类型:IAsyncInitialization``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,除非初始化失败,因此需要显示失败。遗憾的是,没有实现 ,因此任务完成不会自动显示。您可以在 AsyncEx 库中使用类似 NotifyTaskCompletion 类型的类型来简化此操作:Task``INotifyPropertyChanged

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<T>