【发布时间】:2021-11-02 20:04:57
【问题描述】:
.NET 标准库中有很多没有接口的类。它忽略了依赖倒置原则。静态方法也有同样的情况。但我们可以像这样流畅地调整它们
class DateTimeProvider : IDateTimeProvider {
public DateTime GetNow() => DateTime.Now;
}
有时,有必要进行这种调整,尤其是对于单元测试。我找到了另一个理由来调整要接口的类。它是协方差。缺少协方差是痛苦的,最痛苦的例子是Task<T>。协方差类型参数不能在泛型类中声明。并且以下不起作用。
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<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>进行'拆箱'时,我有协方差。以下工作正常:
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>。但事实并非如此。所以,我正在努力适应它。这不是快速解决方案的本地代码问题。我正在实现一个单子异步树,它是我框架的核心部分。我需要 true 协方差。错过它会阻止我。
【问题讨论】:
-
仅供参考,您可以执行
public async Task<object> GetString() => await Task.FromResult("Hello");之类的操作,因为这不起作用public Task<object> GetString() => Task.FromResult("Hello"); -
也许this 可以帮忙?
-
“有必要进行这种改编” - 我不同意,我认为你在这上面浪费时间。虽然
Task<T>远非完美,但它是.NET 和C# 中的一种基本类型,它不应该合法地被抽象或改编掉。你想解决什么真正的问题? -
@Dai 是对的;您尝试做的事情几乎没有意义。实现测试接口的类的优点是便于模拟。但是您不应该测试
DateTime或Task,因此没有理由模拟它们,因此也没有理由让它们实现接口。 -
@ValentineZakharenko C#/.NET 的类型系统仍然太不灵活且表达能力不足,无法支持 ADT、monad 和更高种类的类型(首先要支持像 endofunctor 这样的同态)。过去,我曾尝试过完全按照您现在正在做的方式去做,并且不断遇到这些不符合人体工程学的困难——我最终想出的实用程序类型非常无法使用。我并不是要劝阻你,但我个人会等到 CLR 的类型系统改进之后。不过,您可能可以在 C++ 模板中执行此操作...
标签: c# async-await syntax adapter covariance