基于接口未实现的通用约束

本文关键字:实现 约束 于接口 接口 | 更新日期: 2023-09-27 17:57:59

我有一个带有工厂服务的应用程序,可以在解决必要的依赖注入的同时构建实例。例如,我使用它来构建对话框视图模型。我有一个看起来像这样的服务接口:

public interface IAsyncFactory
{
    Task<T> Build<T>() where T: class, IAsyncInitialize;
}

理想情况下,我想要的是这样的东西(伪语法,因为这不是直接可以实现的)

public interface IFactory
{
    Task<T> Build<T>() where T: class, IAsyncInitialize;
    T Build<T>() where T: class, !IAsyncInitialize;
}

这里的想法是,如果一个类支持IAsyncInitialize,我希望编译器解析为返回Task<T>的方法,这样从消费代码中可以明显看出,它需要等待初始化。如果该类不支持IAsyncInitialize,我想直接返回该类。C#语法不允许这样做,但有没有不同的方法来实现我想要的?这里的主要目标是帮助提醒类的使用者实例化它的正确方法,这样对于具有异步初始化组件的类,我就不会在初始化之前尝试使用它。

我能想到的最接近的方法是创建单独的BuildBuildAsync方法,如果为IAsyncInitialize类型调用Build,则会出现运行时错误,但这没有在编译时捕获错误的好处。

基于接口未实现的通用约束

通常,Microsoft建议在命名异步方法时添加async后缀。因此,您假设创建两个名为BuildBuildAsync的方法是有意义的。

我认为没有办法强制执行"所有没有实现IAsyncInitialize的类型都应该使用Build方法而不是BuildAsync"之类的东西,除非你强迫开发人员用另一个接口(如ISynchronousInitialize)标记同步方法。

您可以尝试以下操作;

  1. 不必分离方法,只需实现一个具有以下签名的BuildAsync方法:

    Task<T> BuildAsync<T>() where T: class
    
  2. BuildAsync方法中,检查T是否实现了IAsyncInitialize。如果是这种情况,只需在创建类型为T的对象后调用相关的初始化代码。否则,只需创建一个TaskCompletionSource对象,并像异步一样运行同步初始化代码。

下面的方法可能不是最好的,但我觉得它非常方便。当异步和同步初始化程序都可用(或者可能可用)时,我用Task.FromResult将同步初始化程序包装为异步,并且只向客户端公开异步方法:

public interface IAsyncInitialize
{
    Task InitAsync();
    int Data { get; }
}
// sync version
class SyncClass : IAsyncInitialize
{
    readonly int _data = 1;
    public Task InitAsync()
    {
        return Task.FromResult(true);
    }
    public int Data { get { return _data; } }
}
// async version
class AsyncClass: IAsyncInitialize
{
    int? _data;
    public async Task InitAsync()
    {
        await Task.Delay(1000);
        _data = 1;
    }
    public int Data
    {
        get 
        {
            if (!_data.HasValue)
                throw new ApplicationException("Data uninitalized.");
            return _data.Value; 
        }
    }
}

这只剩下异步版本的工厂:

public interface IAsyncFactory
{
    // Build can create either SyncClass or AsyncClass
    Task<T> Build<T>() where T: class, IAsyncInitialize;
}

此外,我更喜欢避免像InitAsync这样的专用初始化器方法,而是直接将异步属性公开为任务:

public interface IAsyncData
{
    Task<int> AsyncData { get; }
}
// sync version
class SyncClass : IAsyncData
{
    readonly Task<int> _data = Task.FromResult(1);
    public Task<int> AsyncData
    {
        get { return _data; }
    }
}
// async versions
class AsyncClass : IAsyncData
{
    readonly Task<int> _data = GetDataAsync();
    public Task<int> AsyncData
    {
        get { return _data; }
    }
    private static async Task<int> GetDataAsync()
    {
        await Task.Delay(1000);
        return 1;
    }
}

在任何一种情况下,都会对客户端代码施加异步性,即:

var sum = await provider1.AsyncData + await provider2.AsyncData;

然而,我不认为这是一个问题,因为同步版本的Task.FromResultawait Task.FromResult的开销非常低。我将发布一些基准。

使用异步属性的方法可以通过Lazy<T>进一步改进,例如:

public class AsyncData<T>
{
    readonly Lazy<Task<T>> _data;
    // expose async initializer 
    public AsyncData(Func<Task<T>> asyncInit, bool makeThreadSafe = true)
    {
        _data = new Lazy<Task<T>>(asyncInit, makeThreadSafe);
    }
    // expose sync initializer as async
    public AsyncData(Func<T> syncInit, bool makeThreadSafe = true)
    {
        _data = new Lazy<Task<T>>(() => 
            Task.FromResult(syncInit()), makeThreadSafe);
    }
    public Task<T> AsyncValue
    {
        get { return _data.Value; }
    }
}