C# 5 async: experimenting with member resolution (GetAwaiter, BeginAwait, EndAwait)
Some of you may remember the bizarre query expressions I've come up with before now. These rely on LINQ finding the members it needs (Select, Where, SelectMany etc) statically but without relying on any particular interface. I was pleased to see that the C# 5 async support is based on the same idea. Here's the relevant bit of the draft spec:
The expression t of an await-expression await t is called the task of the await expression. The task t is required to be awaitable, which means that all of the following must hold:
- (t).GetAwaiter() is a valid expression of type A.
- Given an expression a of type A and an expression r of type System.Action, (a).BeginAwait(r) is a valid boolean-expression.
- Given an expression a of type A, (a).EndAwait() is a valid expression.
A is called the awaiter type for the await expression. The GetAwaiter method is used to obtain an awaiter for the task.
The BeginAwait method is used to sign up a continuation on the awaited task. The continuation passed is the resumption delegate, which is further explained below.
The EndAwait method is used to obtain the outcome of the task once it is complete.
The method calls will be resolved syntactically, so all of GetAwaiter, BeginAwait and EndAwait can be either instance members or extension methods, or even bound dynamically, as long as the calls are valid in the context where the await expression appears. All of them are intended to be “non-blocking”; that is, not cause the calling thread to wait for a significant amount of time, e.g. for an operation to complete.
As far as I can tell, either the CTP release hasn't fully implemented this, or I've interpreted a bit overly broadly. Still, let's see what works and what doesn't. For simplicity, each example is completely self-contained... and does absolutely nothing interesting. It's only the resolution part which is interesting. (The fact that the Main method is async is quite amusing though, and takes advantage of the fact that async methods can return void instead of a task.)
Example 1: Boring instance members
This is closest to the examples I've given so far.
using System;
class Test
{
static async void Main()
{
await new Awaitable();
}
}
class Awaitable
{
public Awaiter GetAwaiter()
{
return new Awaiter();
}
}
class Awaiter
{
public bool BeginAwait(Action continuation)
{
return false;
}
public int EndAwait()
{
return 1;
}
}
Hopefully this needs no further explanation. Obviously it works fine with the CTP. The compiler generates a call to GetAwaiter, then calls BeginAwait and EndAwait on the returned Awaiter.
Example 2: Extension methods
The CTP uses extension methods to get an awaiter for existing types such as Task - but I don't think it uses them for BeginAwait/EndAwait. Fortunately, there's nothing to stop us from using them for everything, and there's nothing forcing you to put the extension methods on sensible types, either - as demonstrated below:
using System;
class Test
{
static async void Main()
{
Guid guid = await 5;
Console.WriteLine("Got result: {0}", guid);
}
}
static class Extensions
{
public static string GetAwaiter(this int number)
{
return number.ToString();
}
public static bool BeginAwait(this string text, Action continuation)
{
Console.WriteLine("Waiting for {0} to finish", text);
return false;
}
public static Guid EndAwait(this string text)
{
Console.WriteLine("Finished waiting for {0}", text);
return Guid.NewGuid();
}
}
I should just emphasize that this code is purely for the sake of experimentation. If I ever see anyone actually extending int and string in this way in production code and blaming me for giving them the idea, I'll be very cross.
However, it all does actually work. This example is silly but not particularly exotic. Let's start going a bit further, using dynamic typing.
Example 3: Dynamic resolution
The spec explicitly says that the methods can be bound dynamically, so I'd expect this to work:
using System;
using System.Dynamic;
class Test
{
static async void Main()
{
dynamic d = new ExpandoObject();
d.GetAwaiter = (Func<dynamic>) (() => d);
d.BeginAwait = (Func<Action, bool>) (action => {
Console.WriteLine("Awaiting");
return false;
});
d.EndAwait = (Func<string>)(() => "Finished dynamically");
string result = await d;
Console.WriteLine("Result: {0}", result);
}
}
Unfortunately, in the CTP this doesn't work - it fails at compile time with this error:
Test.cs(16,25): error CS1991: Cannot await 'dynamic'
All is not lost, however. We may not be able to make GetAwaiter to be called dynamically, but what about BeginAwait/EndAwait? Let's try again:
using System;
using System.Dynamic;
class DynamicAwaitable
{
public dynamic GetAwaiter()
{
dynamic d = new ExpandoObject();
d.BeginAwait = (Func<Action, bool>) (action => {
Console.WriteLine("Awaiting");
return false;
});
d.EndAwait = (Func<string>)(() => "Finished dynamically");
return d;
}
}
class Test
{
static async void Main()
{
string result = await new DynamicAwaitable();
Console.WriteLine("Result: {0}", result);
}
}
This time we get more errors:
Test.cs(22,25): error CS1061: 'dynamic' does not contain a definition for 'BeginAwait' and no extension method 'BeginAwait' accepting a first argument of type 'dynamic' could be found (are you missing a using directive or an assembly reference?)
Test.cs(22,25): error CS1061: 'dynamic' does not contain a definition for 'EndAwait' and no extension method 'EndAwait' accepting a first argument of type 'dynamic' could be found (are you missing a using directive or an assembly reference?)
Test.cs(22,25): error CS1986: The 'await' operator requires that its operand 'DynamicAwaitable' have a suitable public GetAwaiter method
This is actually worse than before: not only is it not working as I'd expect to, but even the error message has a bug. The await operator doesn't require that its operand has a suitable public GetAwaiter method - it just has to be accessible. At least, that's the case with the current CTP. In my control flow post for example, the methods were all internal. It's possible that the error message is by design, and the compiler shouldn't have allowed that code, of course - but it would seem a little odd.
Okay, so dynamic resolution doesn't work. Oh well... let's go back to static typing, but use delegates, fields and properties.
Example 4: Fields and properties of delegate types
This time we're back to the style of my original "odd query expressions" post, using fields and properties returning delegates instead of methods:
using System;
class FieldAwaiter
{
public readonly Func<Action, bool> BeginAwait = continuation => false;
public readonly Func<string> EndAwait = () => "Result from a property";
}
class PropertyAwaitable
{
public Func<FieldAwaiter> GetAwaiter
{
get { return () => new FieldAwaiter(); }
}
}
class Test
{
static async void Main()
{
string result = await new PropertyAwaitable();
Console.WriteLine("Result: {0}", result);
}
}
Again, I believe this should work according to the spec. After all, this block of code compiles with no problems:
var t = new PropertyAwaitable();
var a = (t).GetAwaiter();
bool sync = (a).BeginAwait(() => {});
string result = (a).EndAwait();
Unfortunately, nothing doing. The version using await fails with this error:
Test.cs(21,25): error CS1061: 'PropertyAwaitable' does not contain a definition for 'GetAwaiter' and no extension method 'GetAwaiter' accepting a first argument of type 'PropertyAwaitable' could be found (are you missing a using directive or an assembly reference?)
Test.cs(21,25): error CS1986: The 'await' operator requires that its operand 'PropertyAwaitable' have a suitable public GetAwaiter method
This wasn't trying truly weird things like awaiting a class name. Oh well :(
Conclusion
Either I've misread the spec, or the CTP doesn't fully comply to it. This should come as no surprise. It's not a final release or even a beta. However, it's fun to investigate the limits of what should be valid. The next question is whether the compiler should be changed, or the spec... I can't immediately think of any really useful patterns involving returning delegates from properties, for example... so is it really worth changing the compiler to allow it?
Update (7th November 2010)
On Thursday I spoke to Mads Torgersen and Lucian Wischik about this. Some changes being considered:
- The C# spec being tightened up to explicitly say that GetAwaiter/BeginAwait/EndAwait have to be methods, at least when statically typed. In other words, the delegate/property version wouldn't be expected to work.
- The BeginAwait pattern may be tightened up to require a return type of exactly Boolean or dynamic (rather than the call being "a boolean-expression" which is very slightly more lax)
- The dynamic version working - this is more complicated in terms of implementation than one might expect
Just to emphasize, these are changes under consideration rather than promises. They seem entirely reasonable to me. (Dynamic binding sounds potentially useful; property/field resolution definitely less so. Making the spec match the implementation is important to me though :)