Why isn't there an IAwaitable and IAwaiter interface
Asked Answered
T

3

7

I recently learned about the possibility to have custom awaitable types and as this question and Stephen Toub states, there are several requirements to be an awaitable type.

So if a type T wants to be awaitable it must

  • expose an parameterless method GetAwaiter that returns a valid awaiter

and if a type A wants to be a valid awaiter it must

  • Implement the INotifyCompletion interface
  • Provide a boolean property called IsCompleted
  • Provide a parameterless GetResult method that returns void or TResult

So now I'm asking if all that is required to be an awaitable type, why isn't that part of some interfaces like

public interface INotifyCompletion
{
    bool IsCompleted { get; }
    void OnCompleted(Action continuation);
}

public interface IAwaiter : INotifyCompletion
{
    void GetResult();
}

public interface IAwaitable<TAwaiter> where TAwaiter : IAwaiter
{
    TAwaiter GetAwaiter();
}

public interface IAwaiter<TResult> : INotifyCompletion
{
    TResult GetResult();
}

// this would probably not necessary but would likely help to identify
// awaitables that return a value
public interface IAwaitable<TAwaiter, TResult> where TAwaiter : IAwaiter<TResult>
{
    TAwaiter GetAwaiter();
}

I also do understand that the compiler does not need it, since it can check all that at compile time without any penalty. But since there is the INotifyCompletion interface for the OnCompleted() method, why isn't the rest of the interface for awaitables and awaiters packed in some interfaces?

This would most likely help to inform programmers how to implement this.

I also know that one can make an type awaitable by providing an extension method that return a valid awaiter, but again why isn't the whole interface for an awaiter packed in a single interface but has holes (i.e. the IsCompleted property is not part of any interface but required)?

Trinary answered 7/4, 2020 at 8:3 Comment(8)
If I had to guess I'd assume it's because Task existed in the form of the TPL long before async/await did.Lethalethal
The question should be reversed. If all that's needed is to have a GetAwaiter() method, why add an interface? It's not needed. This particular detail won't help most programmers though, as only very specialised code would even need to implement this. Code like the one found in Task and ValueTask. Applications should return one of those types, not try to implement the methodNetsuke
Perhaps a better question would be, why do you want to implement your own GetAwaiter()?Netsuke
@PanagiotisKanavos I was searching for a way to implement some extension methodes for Tasks and was wondering about other types that can be awaited like the ValueTask and how I could minimize code duplication. For examlple I have an WithTimeout() extension method for Task (which works perfectly fine) but I was wondering how I could make this method accessible to other awaitables like ConfiguredTaskAwaitable which would me enable to write code like task.ConfigureAwait(false).WithTimeout(1000) without dulicating any code.Trinary
Here is a blog post by Eric Lippert explaining why a pattern-based approach is used for the foreach statement. TL;DR there are performance penalties associated with interfaces, that are avoided by using patterns. I guess that patterns are used for await for similar reasons.Tritheism
@TheodorZoulias: You can avoid the performance penalty if you make the interfaces generic type constrains. So, rather than doing static IAwaiter<R> Then<T, R>(this IAwaiter<T> src, Func<T, IAwaiter<R>> callback) {, you'd instead do static IR then<T, R, IT, IR>(this IT t, Func<T, IR> callback) where IT: IAwaiter<T> where IR: IAwaiter<R> {, the JIT will then specialize for every type passed in implementing IAwaiter<?> . It also opens up more opportunities to inline.Mithraism
@DerekZiemba that's a lot of complicated code, which is formatted rather poorly when posted as a comment. I would suggest to post it as an answer instead, where the code can be properly formatted, and your arguments will not be restricted by the 500-chars limit.Tritheism
I wouldn't say it relevant to the actual question. Only as a reply to your comment. Just have to make do with the way stackoverflow is.Mithraism
S
3

IAwaiter and IAwaitable interfaces are available if you want to use them.

Note that they are not mandatory to use, but if you feel like they will make your life easier - feel free to use them.

This blog post may be worth a read as well.

Solfeggio answered 7/4, 2020 at 8:30 Comment(0)
I
3

As mijwlls said before, interfaces are available if you want them. But there is a very important reason for the compiler to not require the implementation of a specific interface.

If you implement an interface on a struct. And then return that struct as an instance of the interface. It gets boxed and allocated on the heap. Same with passing it as an parameter if the type of the parameter is the interface itself. This goes for straight up casting to. Boxing costs time and increases pressure on the Garbage collector.

If the compiler required people to implement the IAwaitable interface instead of directly checking the types for members, then it would be impossible to implement something like ValueTask.

Because the return value of GetAwaiter would have to be typed as an interface. Which necessitates allocating it on the heap.

Note that the same applies to the IEnumerator and IEnumerable interfaces too. You don't actually have to implement IEnumerable on a struct or class to use it in a foreach loop. Your type just has to have a parameterless method that returns an instance of a type that has a "Current" property and a parameterless "MoveNext" method that returns a boolean.

Indre answered 12/6, 2022 at 17:14 Comment(0)
H
2

I guess because it would violate the semantic. The async/await is a compile-time feature which implies that the compiler should be aware of what type of awaiter to use in a generated state machine which can be impossible with some interface usage scenarios implying runtime polymorphism when actual type of variable cannot be determined during the compilation. So essentially it means that not all cases can be covered with interface-based awaiters.

Halstead answered 7/4, 2020 at 11:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.