如何调整 Task<T> 以提供协变接口而不丢失 async/await 语法?

How to adapt Task<T> to provide covariant interface without loss of async/await syntax?

.NET 标准库中有很多 classes 没有接口。它忽略了依赖倒置原则。静态方法也有同样的故事。但是我们可以像

一样流畅地改编它们
class DateTimeProvider : IDateTimeProvider {
   public DateTime GetNow() => DateTime.Now;
}

时不时地需要进行这种适配,尤其是单元测试。我找到了另一个调整 class 接口的理由。它是协方差。缺少协方差是痛苦的,最痛苦的例子是Task<T>。协方差类型参数不能在泛型class中声明。而以下不起作用。

class A {}
class B : A {}
...
Task<A> a = GetBAsync();

嗯,看来我需要适配一下,让它多一个接口。但它不像 DateTime 那样容易。额外的一点是 C# 有 async/await 依赖于 Task 的语法结构。我不想失去这个结构。

经过一番调查,我发现可以通过实现一些接口来实现。其中一些接口是 扩展的 (一些特定的 methods/properties 需要实现但不包含在任何 C# 接口中)。

因此,我声明了以下接口(具有协变性)并实现了以下 classes.

interface IAwaiter<out T> : ICriticalNotifyCompletion
{
    bool IsCompleted { get; }

    T GetResult();
}
struct Awaiter<T> : IAwaiter<T>
{
    private readonly TaskAwaiter<T> _origin;

    public Awaiter(TaskAwaiter<T> origin) =>
        _origin = origin;

    public bool IsCompleted =>
        _origin.IsCompleted;

    public T GetResult() =>
        _origin.GetResult();

    public void OnCompleted(Action continuation) =>
        _origin.OnCompleted(continuation);

    public void UnsafeOnCompleted(Action continuation) =>
        _origin.UnsafeOnCompleted(continuation);
}
interface IAsyncJob<out T>
{
    IAwaiter<T> GetAwaiter();
}
struct Job<T> : IAsyncJob<T>
{
    private readonly Task<T> _task;

    public Job(Task<T> task) =>
         _task = task;

    public IAwaiter<T> GetAwaiter() => 
         new Awaiter<T>(_task.GetAwaiter());
}

之后 await 开始使用我的自定义类型。

class A {}
class B : A {}
...
IAsyncJob<B> bJob = new Job<B>(Task.FromResult(new B()));
IAsyncJob<A> aJob = bJob;
A = await a;

太棒了!但是 async 的问题仍然存在。 我无法在以下上下文中使用我的界面 IAsyncJob<T>

async IAsyncJob<B> GetBAsync() { ... }

我对其进行了更深入的调查,发现我们可以通过实现 extensional 接口并将其附加到带有属性的类任务类型来解决问题。经过以下改动,开始可以编译了。

public class JobBuilder<T>
{
    public static JobBuilder<T> Create() => null;

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine { }

    public void SetStateMachine(IAsyncStateMachine stateMachine) { }

    public void SetResult(T result) { }

    public void SetException(Exception exception) { }

    public IAsyncJob<T> Task => default(IAsyncJob<T>);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : INotifyCompletion
            where TStateMachine : IAsyncStateMachine { }

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine { }
}
[AsyncMethodBuilder(typeof(JobBuilder<>))]
public interface IAsyncJob<out T>
{
    IAsyncJobAwaiter<T> GetAwaiter();
}

显然,我的 JobBuilder<T> 是一个存根,我需要一些实现,使我的代码不仅可编译而且可行。它应该与 Task<T>.

的默认行为相同

也许,有一些 Builder 实现默认用于 Task<T>,我可以委托调用它(我没有找到这个实现)。

如何实现 JobBuilder<T>IAsyncJob<T> 与 .Net 提供的 Task<T> 相同?


注: 当然,当我用 'await' 做 Task<T> 的 'unboxing' 时,我有协方差。以下工作正常:

A a = await GetBAsync();
IEnumerable<A> aCollection = await GetBCollectionAsync();

此外,执行任何转换(包括在执行级别进行转换)都不是问题。

static async Task<TOut> Map<TIn, TOut>(
    this Task<TIn> source,
    Func<TIn, TOut> f) => 
        Task.FromResult(f(await source));
...
var someNumber = await Task
    .Run(() => 42)
    .Map(x => (double)x)
    .Map(x => x * 2.2);

但是当我想在类型系统级别(在输出中使用 Task<T> 的另一个接口内)具有协方差时,这没什么。

下面还是协变的

public interface IJobAsyncGetter<out T>
{
    IAsyncJob<T> GetAsync();
}

但是

public interface ITaskAsyncGetter<out T>
{
    Task<T> GetAsync();
}

不是。

它从解决方案设计能力中排除协变。

IEnumerable<IJobAsyncGetter<B>> b = ...
IEnumerable<IJobAsyncGetter<B>> a = a;

有效,但是

IEnumerable<ITaskAsyncGetter<B>> b = ...
IEnumerable<ITaskAsyncGetter<B>> a = a;

没有。

看起来 Task<T> 是因为向后兼容性而一直存在的 .NET 错误之一。我明白了

public interface IAsyncEnumerable<out T>

是协变且可等待的接口。

我确信可以用同样的方式提供 ITask<T>。但事实并非如此。所以,我正在努力适应它。这不是快速解决方案的本地代码问题。我正在实现一个 monadic 异步树,它是我框架的核心部分。我需要 true 协方差。缺少它阻止了我。

坦率地说,我不知道如何实现您正在寻找的东西。但是,有一个使用最少语法的简单解决方法。这个想法是你等待 GetBAsyncB 转换为 A,将 A 包装在任务中,然后 return 它。这是一个额外的步骤,但很简单:

Task<A> a = Task.Run(async () => (A)await b.GetBAsync());

这样您就可以完全保留 async/await 功能。

与其经历所有这些,我建议为该特定问题编写简单的包装函数:

// Sample classes
class Parent { }
class Child : Parent { }

public class Program
{
    static async Task Main(string[] args)
    {
        Task<Parent> taskToGetChild = As<Child, Parent>(GetChildAsync);
        // some code
        var b = await taskToGetChild;
    }

    static async Task<Child> GetChildAsync()
    {
        return await Task.FromResult(new Child());
    }

    // Here's the method that allow you to mimic covariance.
    static async Task<TOut> As<TIn, TOut>(Func<Task<TIn>> func)
        where TIn : TOut
    {
        return (TOut)await func();
    }
}

您仍然可以在一定程度上调整 Task<T>,而无需实现自定义的类任务类型。 IAsyncEnumerator<T>可以给你一点提示。

我会像这样定义一个接口:

interface IAsync<out T>
{
    T Result { get; }
    
    Task WaitAsync();
}

使用 Task<T>:

的包装器实现接口
public class TaskWrapper<T> : IAsync<T>
{
    private readonly Task<T> _task;

    public TaskWrapper(Task<T> task)
    {
        _task = task;
    }

    public T Result => _task.Result;

    public async Task WaitAsync()
    {
        await _task;
    }
}

添加一个扩展方法来完成异步操作和return一个Task结果:

public static class AsyncExtensions
{
    public static async Task<T> GetResultAsync<T>(this IAsync<T> task)
    {
        await task.WaitAsync();
        return task.Result;
    }
}

根据以上内容,您可以在代码中执行:

class A { }
class B : A { }

IAsync<B> b = new TaskWrapper<B>(Task.FromResult<B>(new B()));
IAsync<A> a = b;

IEnumerable<IAsync<B>> bList = new List<IAsync<B>>();
IEnumerable<IAsync<A>> aList = bList;

A aValue = await a.GetResultAsync();

基于 @GuruStron's comment, I found this repo 完全包含我所询问的内容。因此,Task<T> 的默认生成器是 System.Runtime.CompilerServices.AsyncTaskMethodBuilder<T>.

默认构建器的适配器可以实现如下

public struct JobBuilder<T>
{
    private readonly AsyncTaskMethodBuilder<T> _origin;

    private JobBuilder(AsyncTaskMethodBuilder<T> origin) =>
        _origin = origin;

    public IAsyncJob<T> Task => new Job<T>(_origin.Task);

    public static Builder<T> Create() =>
        new Builder<T>(AsyncTaskMethodBuilder<T>.Create());

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine =>
            _origin.Start(ref stateMachine);

    public void SetStateMachine(IAsyncStateMachine stateMachine) =>
        _origin.SetStateMachine(stateMachine);

    public void SetResult(T result) =>
        _origin.SetResult(result);

    public void SetException(Exception exception) =>
        _origin.SetException(exception);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : INotifyCompletion
            where TStateMachine : IAsyncStateMachine =>
                _origin.AwaitOnCompleted(ref awaiter, ref stateMachine);

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine =>
                _origin.AwaitOnCompleted(ref awaiter, ref stateMachine);
}