【问题标题】:How to adapt Task<T> to provide covariant interface without loss of async/await syntax?如何在不丢失异步/等待语法的情况下调整 Task<T> 以提供协变接口?
【发布时间】:2021-11-02 20:04:57
【问题描述】:

.NET 标准库中有很多没有接口的类。它忽略了依赖倒置原则。静态方法也有同样的情况。但我们可以像这样流畅地调整它们

class DateTimeProvider : IDateTimeProvider {
   public DateTime GetNow() => DateTime.Now;
}

有时,有必要进行这种调整,尤其是对于单元测试。我找到了另一个理由来调整要接口的类。它是协方差。缺少协方差是痛苦的,最痛苦的例子是Task&lt;T&gt;。协方差类型参数不能在泛型类中声明。并且以下不起作用。

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

嗯,看来我需要对其进行调整以使其具有额外的界面。但这并不像使用 DateTime 那样容易。额外的一点是C#有async/await语法结构,它依赖于Task。我不想失去这个结构。

经过一番调查,我发现可以通过实现一些接口来做到这一点。其中一些接口是扩展的(一些特定的方法/属性是必须实现的,但不包含在任何 C# 接口中)。

所以,我声明了以下接口(具有协方差)并实现了以下类。

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&lt;T&gt;

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&lt;T&gt; 是一个存根,我需要一些实现来使我的代码不仅可编译而且可行。它应该与Task&lt;T&gt; 的默认行为相同。

也许,有一些 Builder 实现默认用于 Task&lt;T&gt;,我可以将调用委托给它(我没有找到这个实现)。

如何实现JobBuilder&lt;T&gt; 以使IAsyncJob&lt;T&gt; 与.Net 为Task&lt;T&gt; 提供的相同?


注意: 当然,当我用'await'对Task&lt;T&gt;进行'拆箱'时,我有协方差。以下工作正常:

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&lt;T&gt; 输出的接口内)有协方差时,这没什么。

以下仍然是协变的

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&lt;T&gt; 是由于向后兼容性而与我们共存的 .NET 错误之一。我明白了

public interface IAsyncEnumerable<out T>

是协变且可等待的接口。

我确信有可能以同样的方式提供ITask&lt;T&gt;。但事实并非如此。所以,我正在努力适应它。这不是快速解决方案的本地代码问题。我正在实现一个单子异步树,它是我框架的核心部分。我需要 true 协方差。错过它会阻止我。

【问题讨论】:

  • 仅供参考,您可以执行public async Task&lt;object&gt; GetString() =&gt; await Task.FromResult("Hello"); 之类的操作,因为这不起作用public Task&lt;object&gt; GetString() =&gt; Task.FromResult("Hello");
  • 也许this 可以帮忙?
  • “有必要进行这种改编” - 我不同意,我认为你在这上面浪费时间。虽然Task&lt;T&gt; 远非完美,但它是.NET 和C# 中的一种基本类型,它不应该合法地被抽象或改编掉。你想解决什么真正的问题
  • @Dai 是对的;您尝试做的事情几乎没有意义。实现测试接口的类的优点是便于模拟。但是您不应该测试DateTimeTask,因此没有理由模拟它们,因此也没有理由让它们实现接口。
  • @ValentineZakharenko C#/.NET 的类型系统仍然太不灵活且表达能力不足,无法支持 ADT、monad 和更高种类的类型(首先要支持像 endofunctor 这样的同态)。过去,我曾尝试过完全按照您现在正在做的方式去做,并且不断遇到这些不符合人体工程学的困难——我最终想出的实用程序类型非常无法使用。我并不是要劝阻你,但我个人会等到 CLR 的类型系统改进之后。不过,您可能可以在 C++ 模板中执行此操作...

标签: c# async-await syntax adapter covariance


【解决方案1】:

根据@GuruStron 的评论,我发现this repo 包含我所询问的内容。因此,Task&lt;T&gt; 的默认构建器是 System.Runtime.CompilerServices.AsyncTaskMethodBuilder&lt;T&gt;

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

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);
}

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      【解决方案3】:

      我建议不要为这个特定问题编写简单的包装函数,而不是经历所有这些:

      // 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();
          }
      }
      

      【讨论】:

      • await Task.FromResult 是多余的。恕我直言,您应该只拥有static async Task&lt;TOut&gt; As&lt;TIn, TOut&gt;(Task&lt;TIn&gt; t) where TIn : TOut =&gt; (TOut)await t;
      • @JeremyLakeman 好建议,谢谢 :)
      【解决方案4】:

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

      我会这样定义一个接口:

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

      Task&lt;T&gt;的包装器实现接口:

      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;
          }
      }
      

      添加一个扩展方法来完成异步操作并返回一个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();
      

      【讨论】:

        猜你喜欢
        • 2020-01-16
        • 1970-01-01
        • 1970-01-01
        • 2021-12-19
        • 1970-01-01
        • 2016-07-23
        相关资源
        最近更新 更多