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)?

Ryan Thomas
  • 1,724
  • 2
  • 14
  • 27
Ackdari
  • 3,222
  • 1
  • 16
  • 33
  • 1
    If I had to guess I'd assume it's because Task existed in the form of the TPL long before async/await did. – GazTheDestroyer Apr 07 '20 at 08:08
  • 1
    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 method – Panagiotis Kanavos Apr 07 '20 at 08:14
  • Perhaps a better question would be, why do you want to implement your own `GetAwaiter()`? – Panagiotis Kanavos Apr 07 '20 at 08:15
  • 1
    @PanagiotisKanavos I was searching for a way to implement some extension methodes for `Task`s 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. – Ackdari Apr 07 '20 at 08:34
  • 3
    [Here](https://ericlippert.com/2011/06/30/following-the-pattern/) 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. – Theodor Zoulias Apr 07 '20 at 11:27
  • @TheodorZoulias: You can avoid the performance penalty if you make the interfaces generic type constrains. So, rather than doing `static IAwaiter Then(this IAwaiter src, Func> callback) {`, you'd instead do `static IR then(this IT t, Func callback) where IT: IAwaiter where IR: IAwaiter {`, the JIT will then specialize for every type passed in implementing `IAwaiter>` . It also opens up more opportunities to inline. – Derek Ziemba Apr 29 '22 at 16:55
  • @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. – Theodor Zoulias Apr 29 '22 at 17:31
  • 1
    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. – Derek Ziemba Apr 30 '22 at 18:52

3 Answers3

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.

mjwills
  • 23,389
  • 6
  • 40
  • 63
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.

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.

Dmytro Mukalov
  • 1,949
  • 1
  • 9
  • 14