Issue
There are plenty of classes in .NET standard library that have no interfaces. It ignores the dependency inversion principle. There is the same story with static methods. But we can fluently adapt them like
class DateTimeProvider : IDateTimeProvider {
public DateTime GetNow() => DateTime.Now;
}
From time to time, it is necessary to make this adaptation, especially for unit testing. I found one more reason to adapt the class to be interfaced. It is covariance. Missing covariance is painful, and the most painful example is Task<T>
.The covariance type parameter can't be declared in the generic class. And the following doesn't work.
class A {}
class B : A {}
...
Task<A> a = GetBAsync();
Hmm, it looks like I need to adapt it to make it has an additional interface. But it isn't so easy as with DateTime. The extra point is that C# has async/await
syntax construction that depends on Task. I don't want to lose this construction.
After some investigation, I found out that it is possible to do it by implementing some interfaces. Some of those interfaces are extensional (some specific methods/properties that are necessary to be implemented but not included in any C# interface).
So, I have declared the following interfaces (to have covariance) and implemented the following 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());
}
After that await
started to work with my custom type.
class A {}
class B : A {}
...
IAsyncJob<B> bJob = new Job<B>(Task.FromResult(new B()));
IAsyncJob<A> aJob = bJob;
A = await a;
Great! But the problem with async
still exists.
I can't use my interface IAsyncJob<T>
in the following context:
async IAsyncJob<B> GetBAsync() { ... }
I investigated it deeper and found out that we can solve the problem by implementing the extensional interface and attaching it to my task-like type with attribute. After the following changes, it started to be compilable.
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();
}
Obviously, my JobBuilder<T>
is a stub and I need some implementation that will make my code not only compilable but workable. It should work the same as the default behavior for Task<T>
.
Maybe, there is some Builder implementation that is used for Task<T>
by default and I can delegate calls to it (I didn't find this implementation).
How to implement JobBuilder<T>
to have the same for IAsyncJob<T>
as .Net provides for Task<T>
?
Note:
Of course, I have covariance when I do 'unboxing' of Task<T>
with 'await'. And the following works fine:
A a = await GetBAsync();
IEnumerable<A> aCollection = await GetBCollectionAsync();
Also, it isn't a problem to do any conversions including casting at the execution level.
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);
But it is nothing when I want to have covariance in type system level (inside another interface that uses Task<T>
in outputs).
The following is still covariant
public interface IJobAsyncGetter<out T>
{
IAsyncJob<T> GetAsync();
}
but
public interface ITaskAsyncGetter<out T>
{
Task<T> GetAsync();
}
is not.
It excludes covariants from solution design abilities.
And
IEnumerable<IJobAsyncGetter<B>> b = ...
IEnumerable<IJobAsyncGetter<A>> a = b;
works, but
IEnumerable<ITaskAsyncGetter<B>> b = ...
IEnumerable<ITaskAsyncGetter<A>> a = b;
doesn't.
It looks like Task<T>
is one of .NET mistakes that lives with us because of backward compatibility. I see
public interface IAsyncEnumerable<out T>
is covariant and awaitable interface that works.
I am sure there was possible to provide ITask<T>
in the same way. But it has not been. So, I am trying to adapt it. It isn't a local code problem for a quick solution. I am implementing a monadic async tree, and it is a core part of my framework. I need true covariance. And missing of it blocks me.
Solution
Based on the @GuruStron's comment, I found this repo that contains exactly what I was asking about. So, the default builder for Task<T>
is System.Runtime.CompilerServices.AsyncTaskMethodBuilder<T>
.
The adapter of the default builder can be implemented as the following
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.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
}
Answered By - Valentine Zakharenko Answer Checked By - Robin (PHPFixing Admin)
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.