如何调整 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 协方差。缺少它阻止了我。
坦率地说,我不知道如何实现您正在寻找的东西。但是,有一个使用最少语法的简单解决方法。这个想法是你等待 GetBAsync
将 B
转换为 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);
}
.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 协方差。缺少它阻止了我。
坦率地说,我不知道如何实现您正在寻找的东西。但是,有一个使用最少语法的简单解决方法。这个想法是你等待 GetBAsync
将 B
转换为 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);
}