In C#6, async methods must return either void
or Task
or Task<T>
. This proposed feature allows them to return any tasklike type.
- TRY IT OUT ONLINE: tryroslyn.azurewebsites.net
- Download: ArbitraryAsyncReturns.zip [22mb]
- Install in VS2015: From the zip-file, double-click on RoslynDeployment.VSIX
- Install in .NETCore (OSX/Linux/Windows): From the zip-file, copy the DLLs into (OSX)
/usr/local/share/dotnet/shared/Microsoft.NETCore.App/1.0.0-rc2-3002702
or (Windows)C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.0-rc2-3002702
- Test: In the zip-file there are sample projects for VS2015 and for .NETCore.
- Uninstall: For VS2015 go to Tools>Extensions, search for Roslyn, and uninstall the preview. For .NETCore you're on your own!
- Watch: I coded this prototype live in livecoding.tv, and you can watch recordings if you want: livecoding.tv/ljw1004
- Discuss: please read the [Design rationale and alternatives](feature - arbitrary async returns - discussion.md), and then go to the discussion thread
The primary benefit is to allow a ValueTask<T>
that reduces the "async overhead cost" on the hot path:

- Such a
ValueTask<T>
has already been checked into corefx (corefx#4857) - Some teams decide the perf benefits of
ValueTask<T>
are so great that they're worth the cumbersome manual code you have to write today (rightmost column) -- e.g.System.Xml
(corefx#4936), which had previously been using an internal form ofValueTask
. - Other teams decide that the cumbersome code is so ugly they're willing to forego the perf benefits for now, at least until the C# compiler gets support for
ValueTask<T>
-- e.g. ASP.NET FormReader (aspnet#556). UsingValueTask<T>
would have saved 10% of heap allocations for 3k requests, up to 90% for 30k requests.
(This feature proposal merely allows the middle column, to remove the necessity of heap allocation and to improve perf somewhat. Could we build upon this feature proposal to allow the rightmost column, with additional compiler unrolling to achieve optimum perf? It seems hard because the unrolled version has different semantics aroud context-capture, and the incremental perf wins seem comparitively minor. See issue#10449.)
The secondary benefit is to allow async methods to return other domain-specific tasklike values. Here are some examples. However, they won't be allowed with the current proposal until such time as "extension static methods" get added to C#.
// IAsyncAction in Windows programming. Today you write wrapper code like this:
// IAsyncAction TestAsync(int p, int q) => TestAsyncInner(p, q).AsAsyncAction();
// async Task TestAsyncInner(int p, int q) { ... }
// It would be neater if you could just return directly:
async IAsyncAction TestAsync(int p, int q) { ... }
// ITask<out T>. The Task that we're forced to use today is invariant. Sometimes you want covariance:
async ITask<string> TestAsync() { ... }
ITask<object> = TestAsync();
// IObservable. Today you write wrapper code like this:
// IObservable<int> TestAsync(int p, int q) => TestAsyncInner(p,q).ToObservable();
// async Task<int> TestAsyncInner(int p, int q) { ... }
// It would be neater if you could just return directly:
async IObservable<int> TestAsync(int p, int q) { ... }
Actually that IObservable
example is still up for discussion. It's not clear whether IObservable
would prefer to be like an async method or an async enumerable method. We need to hear from IObservable
experts.
A third "benefit" (it's arguable whether this is a benefit at all) is that people will be able to weave hooks into their async methods, for instance to call into their own function before and after every cold-path await:
async InstrumentedTask TestAsync()
{
await InstrumentedTask.Configure(actionBeforeEachAwait, actionAfterEachAwait);
await Task.Delay(10);
await Task.Delay(20);
}
Some other examples: implement a C# version of Haskell's Maybe
monad with do
notation [link]; implement async iterator methods [link].
These use-cases are written out as unit-tests at the end of this proposal. Here they are in a nutshell, in order of importance:
- I should be able to use
ValueTask
as a wholesale replacement forTask
, every bit as good. [link] - I should be able to migrate my existing API over to
ValueTask
, maintaining source-compatibility and binary-compatibility. [link] - I don't want to break backwards-compatibility. In particular, suppose I have a C#6 app that references a NuGet library in which
ValueTask
is already tasklike. When I upgrade to C#7, I don't want the behavior of my code to change. [link]
Rule 1: Tasklike. Define:
- A non-generic tasklike is any non-generic type with a single public static method
CreateAsyncMethodBuilder()
, or the typeSystem.Threading.Tasks.Task
. - A generic tasklike is any generic type with arity 1 with the same method, or the type
System.Threading.Tasks.Task<T>
.
struct ValueTask<T>
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTaskBuilder<T> CreateAsyncMethodBuilder() { ... }
...
}
This uses a static method
CreateAsyncMethodBuilder()
to distinguish a Tasklike type and locate its builder. I've also suggested some other techniques here: (1) static method, (2) extension method, (3) attribute on tasklike type, (4) attribute on async method. I believe a static method is best, and when C# eventually gets extension static methods then it should use them too.
Rule 2: async methods. The rules for async functions currently allow an async method to return either void
or Task
or Task<T>
; this will be changed to allow it to return either void
, or any non-generic Tasklike
, or generic Tasklike<T>
.
async ValueTask<int> TaskAsync(int delay)
{
await Task.Delay(delay);
return 10;
}
Rule 3: async lambdas. The rules for anonymous function conversion currently allow an async lambda to be converted to a delegate type whose return type is either void
or Task
or Task<T>
; this will be changed to let them its return type be void
or any non-generic Tasklike
or any generic Tasklike<T>
. (Note that any new conversions introduce ambiguity errors; see the discussion on this matter).
Func<int, ValueTask<int>> lambda = async (x) => { return x; };
Rule 4: inferred return type. The inferred return type of a lambda expression currently takes into account the parameter types of the delegate to which the lambda is being converted. To make type inference aware of the new conversion in Rule 3, the inferred return type of an async lambda now also takes into account the return type of that delegate:
- If the async lambda has inferred result type
void
:- if the return type of the delegate is
U
whereU
is a non-generic tasklike, then the inferred return type isU
- otherwise the inferred return type is
Task
- if the return type of the delegate is
- Otherwise, the async lambda has inferred result type
V1
:- if the return type of the delegate is
U<V2>
whereU
is a generic tasklike, then the inferred return type isU<V1>
- otherwise the inferred return type is
Task<V1>
- if the return type of the delegate is
f(async (x) => {return x;})
void f<T>(Func<int,T> lambda); // inferred lambda return type is Task<int>, giving T = Task<int>
void f<U>(Func<int,Task<U>> lambda); // inferred lambda return type is Task<int>, giving U = int
void f<U>(Func<int,ValueTask<U>> lambda); // currently: inferred lambda return type is Task<int>, giving a type inference failure
void f<U>(Func<int,ValueTask<U>> lambda); // proposal: inferred lambda return type is ValueTask<int>, giving U = int
Rule 5: overload resolution. This is subtle. It's dealt with below.
Rule 6: evaluation of async functions. The rules for evaluation of task-returning async functions currently talk in general terms about "generating an instance of the returned task type" and "initially incomplete state" and "moved out of the incomplete state". These will be changed to spell out how that returned tasklike is constructed and how its state is transitioned, as detailed in the following subsection.
struct ValueTaskBuilder<T>
{
public static MyTaskBuilder<T> Create();
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void Start<TSM>(ref TSM stateMachine) where TSM:IAsyncStateMachine;
public void AwaitOnCompleted<TA, TSM>(ref TA awaiter, ref TSM stateMachine) where TA:INotifyCompletion where TSM:IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TA, TSM>(ref TA awaiter, ref TSM stateMachine) where TA:ICriticalNotifyCompletion where TSM:IAsyncStateMachine;
public void SetResult(T result);
public void SetException(Exception ex);
public ValueTask<T> Task {get;}
}
The builder type of a tasklike is the return type of the static method CreateAsyncMethodBuilder()
on that tasklike. (Except: if the tasklike is System.Threading.Tasks.Task
then the builder type is System.Runtime.CompilerService.AsyncTaskMethodBuilder
; and if the tasklike is System.Threading.Tasks.Task<T>
then the builder type is System.Runtime.CompilerService.AsyncTaskMethodBuilder<T>
).
When an async tasklike-returning method is invoked,
- It creates
var sm = new CompilerGeneratedStateMachineType()
where this compiler-generated state machine type represents the async tasklike method, and may be a struct or a class, and has a fieldBuilderType builder
in it, and implementsIAsyncStateMachine
. - It assigns
sm.builder = Tasklike.CreateAsyncMethodBuilder()
to create a new instance of the builder type. (Except: if the tasklike isTask
orTask<T>
, then the assignment is insteadsm.builder = BuilderType.Create()
.) - It then calls the
void Start<TSM>(ref TSM sm) where TSM : IAsyncStateMachine
method onbuilder
. It is an error if this instance method doesn't exist or isn't public or has a different signature or constraints. Thesm
variable is that samesm
as was constructed earlier. Upon being given thissm
variable, the builder must invokesm.MoveNext()
on it exactly once, either now in theStart
method or in the future. - It then retrieves the
U Task {get;}
property onsm.builder
. The value of this property is then returned from the async method. It is an error if this instance property doesn't exist or isn't public or if its property typeU
isn't identical to the return type of the async tasklike-returning method.
Execution of sm.MoveNext()
might cause other builder methods to be invoked:
- If the async method completes succesfully, it invokes the method
void SetResult()
onsm.builder
(in case of a non-generic tasklike), or thevoid SetResult(T result)
method with the operand of the return statement (in case of a generic tasklike). It is an error if this instance method doesn't exist or isn't public. - If the async method fails with an exception, it invokes the method
void SetException(System.Exception ex)
onsm.builder
. It is an error if this instance method doesn't exist or isn't public. - If the async method executes an
await e
operation, it invokesvar awaiter = e.GetAwaiter()
.- If this awaiter implements
ICriticalNotifyCompletion
and theIsCompleted
property is false, then it calls the methodvoid AwaitUnsafeOnCompleted<TA,TSM>(ref TA awaiter, ref TSM sm) where TA : ICriticalNotifyCompletion where TSM : IAsyncStateMachine
onsm.builder
. It is an error if this instance method doesn't exist or isn't public or has the wrong constraints. The builder is expected to callawaiter.UnsafeOnCompleted(action)
with someaction
that will causesm.MoveNext()
to be invoked once; or, instead, the builder may callsm.MoveNext()
once itself. - If this awaiter implements
INotifyCompletion
and theIsCompleted
property is false, then it calls the methodvoid AwaitOnCompleted<TA,TSM>(ref TA awaiter, ref TSM sm) where TA : INotifyCompletion where TSM : IAsyncStateMachine
onsm.builder
. It is an error if this instance method doesn't exist or isn't public or has the wrong constraints. Again the builder is expected to callawaiter.OnCompleted(action)
similarly, or callsm.MoveNext()
itself.
- If this awaiter implements
In the case where the builder type is a struct, and sm
is also a struct, it's important to consider what happens should the builder decide to box the struct, e.g. by doing IAsyncStateMachine boxed_sm = sm
. This will always create a new copy of sm
, which will in turn contain a new copy of sm.builder
.
- The builder is at liberty anytime to call
boxed_sm.SetStateMachine(boxed_sm)
. The implementation of this method is compiler-generated, but its only effect is to invoke thevoid SetStateMachine(IAsyncStateMachine boxed_sm)
method onboxed_sm.builder
. It is an error if this instance method on the builder doesn't exist or isn't public. - This mechanism is typically used by struct builder types so they can box only once the
sm
parameter they receive in theirStart
orAwaitOnCompleted
orAwaitUnsafeOnCompleted
methods; on subsequent calls, they ignore that parameter and used the version they have already boxed.
Here informally are the proposed changes to overload resolution. I've written out a digest of the existing rules of overload resolution, and added in empasis the new parts from this proposal.
- If the arguments exactly match one candidate, it wins. An async lambda
async () => 3
is considered an exact match for a delegate with return typeTask<int>
and any otherTasklike<int>
; it's never an exact match for a void-returning delegate. - Otherwise [Better conversion target], if neither or both are an exact match and there's an implicit conversion from one parameter type but not vice versa, then the "from" parameter wins. Otherwise recursively dig in: if both types are delegates then dig into their return types and prefer non-void over void; if the types are
TasklikeA<S1>
andTasklikeB<S2>
then dig intoS1/S2
. - Otherwise [Better function member], if the two candidates have identical parameter types up to all tasklikes being considered the same but one candidate before substitution is more specific then prefer it.
Note that these unit tests are aspirational for the language feature. Not all of them are satisfied by the current proposal. Indeed, some of them are mutually contradictory!
TEST a1: async methods should be able to return ValueTask
async Task<int> a1() { ... } // <-- I can write this in C#6 using Task
async ValueTask<int> a1() { ... } // <-- I should be able to write this instead with the same method body
async Task a1() { ... } // <-- I can write this in C#6 using Task
async ValueTask a1() { ... } // <-- I should be able to write this instead with the same method body
TEST a2: async lambdas should be able to return ValueTask
Func<Task<int>> a2 = async () => { ... }; // <-- I can write this in C#6
Func<ValueTask<int>> a2 = async () => { ... }; // <-- I should be able to write this instead
Func<Task> a2n = async () => { ... }; // <-- I can write this in C#6
Func<ValueTask> a2n = async () => { ... }; // <-- I should be able to write this instead
TEST a3: async lambdas are applicable in overload resolution
a3(async () => 3);
void a3(Func<Task<int>> lambda) // <-- This can be invoked in C#6
void a3(Func<ValueTask<int>> lambda) // <-- If I write this instead, it should be invocable
a3n(async () => {} );
void a3n(Func<Task> lambda) // <-- This can be invoked in C#6
void a3n(Func<ValueTask> lambda) // <-- If I write this instead, it should be invocable
TEST a4: async lambda type inference should work with ValueTask
like it does with Task
a4(async () => 3);
void a4<T>(Func<Task<T>> lambda) // <-- This infers T=int
void a4<T>(Func<ValueTask<T>> lambda) // <-- If I write this instead, it should also infer T=int
TEST a5: able to write overloads that take sync and async lambdas
void a5<T>(Func<T> lambda)
void a5<T>(Func<ValueTask<T>> lambda)
a5(() => 3); // <-- This should invoke the first overload
a5(async () => 3); // <-- This should invoke the second overload
TEST a6: exact match better candidate
void a6(Func<ValueTask<int>> lambda)
void a6(Func<ValueTask<double>> lambda)
a6(async () => 3); // <-- This should prefer the "int" candidate
TEST a7: dig into better candidate
void a7(Func<ValueTask<short>> lambda)
void a7(Func<ValueTask<byte>> lambda)
a7(async () => 3); // <-- This should prefer the "byte" candidate
TEST a8: prefer over void
void a8(Action lambda)
void a8(Func<ValueTask> lambda)
a8(async () => {}); // <-- This should prefer the "ValueTask" candidate
As I migrate, I want to maintaining source-compatibility and binary-compatibility for users of my API. And the reason I'm migrating is because I want the better performance of ValueTask
, so I want users to get that as easily as possible.
TEST b1: change async return type to be ValueTask
async Task<int> b1() // <-- library v1 has this API
async ValueTask<int> b1() // <-- library v2 has this API *instead*
var v = await b1(); // <-- This code will of course work in v2 of the library
Task<int> t = b1(); // <-- Either this code should work in v2 of the library...
Task<int> t = b1().AsTask(); // <-- or this one as a workaround
async Task b1n() // <-- library v1 has this API
async ValueTask b1n() // <-- library v2 has this API *instead*
await b1n(); // <-- This code will of course work in v2 of the library
Task t= b1n(); // <-- Either this could should work in v2 of the library...
Task t = b1n().AsTask(); // <-- or this one as a workaround
TEST b2: add overload where async return type is ValueTask
. [this test is doomed to fail]
async Task<int> b2() // <-- library v1 has this API
async ValueTask<int> b2() // <-- library v2 has this API *additionally*
var t = b2(); // <-- This code should work on either version of the library
async Task b2n() // <-- library v1 has this API
async ValueTask b2n() // <-- library v2 has this API *additionally*
var t = b2n(); // <-- This code should work on either version of the library
TEST b3: change argument to ValueTask
void b3(Task<int> t) // <-- library has this API
ValueTask<int> vt;
b3(vt); // <-- This code should work...
b3(vt.AsTask()); // <-- or, if not, then at least this one should
void b3n(Task t) // <-- library has this API
ValueTask vt;
b3n(vt) // <-- This code should work...
b3n(vt.AsTask()); // <-- or, if not, then at least this one should
TEST b4: change parameter to ValueTask
void b4(Task<int> t) // <-- library v1 has this API
void b4(ValueTask<int> t) // <-- library v2 has this API *instead*
Task<int> t;
b4(t); // <-- Either this code should work in v2 of the library...
b4(t.AsValueTask()); // <-- or this one as a workaround
void b4n(Task t) // <-- library v1 has this API
void b4n(ValueTask t) // <-- library v2 has this API instead
Task t;
b4n(t); // <-- Either this code should work in v2 of the library...
b4n(t.AsValueTask()); // <-- or this one as a workaround
TEST b5: add overload with parameter ValueTask
void b5(Task<int> t) // <-- library v1 has this API
void b5(ValueTask<int> t) // <-- library v2 has this API *additionally*
Task<int> t;
b5(t); // <-- This could should work in v2 of the library and pick the Task overload
ValueTask<int> vt;
b5(vt); // <-- This code should work in v2 of the library and pick ValueTask overload
void b5n(Task t) // <-- library v1 has this API
void b5n(ValueTask t) // <-- library v2 has this API *additionally*
Task t;
b5n(t); // <-- This code should work in v2 of the library and pick the Task overload
ValueTask vt;
b5n(vt); // <-- This code should work in v2 of the library and pick ValueTask overload
TEST b6: change parameter to Func<ValueTask<T>>
void b6(Func<Task<int>> lambda) // <-- library v1 has this API
void b6(Func<ValueTask<int>> lambda) // <-- library v2 has this API *instead*
b6(async () => 3); // <-- This code should work in v2 of the library
void b6n(Func<Task> lambda) // <-- library v1 has this API
void b6n(Func<ValueTask> lambda) // <-- library v2 has this API *instead*
b6n(async () => {}); // <-- This code should work in v2 of the library
TEST b7: add overload with parameter Func<ValueTask<T>>
.
void b7(Func<Task<int>> lambda) // <-- library v1 has this API
void b7(Func<ValueTask<int>> lambda) // <-- library v2 has this API *additionally*
b7(async () => 3); // <-- This code should work in v2 and pick the ValueTask overload, for efficiency
void b7g<T>(Func<Task<T>> lambda) // <-- library v1 has this API
void b7g<T>(Func<ValueTask<T>> lambda) // <-- library v2 has this API *additionally*
b7g(async () => 3); // <-- This code should work in v2 and pick the ValueTask overload, for efficiency
void b7n(Func<Task> lambda) // <-- library v1 has this API
void b7n(Func<ValueTask> lambda) // <-- library v2 has this API *additionally*
b7n(async () => {}); // <-- This code should work in v2 and pick the ValueTask overload, for efficiency
In particular, suppose I have a C#6 app that references a NuGet library in which ValueTask
is already tasklike. When I upgrade my project to C#7, I don't want the behavior of my code to change.
TEST c1: don't now prefer a previously-inapplicable ValueTask
due to exact match. [This test fails under the current proposal]
void c1(Func<Task<double>> lambda)
void c1(Func<ValueTask<int>> lambda)
c1(async () => 3); // <-- When I upgrade, this should still pick the Task<double> overload
TEST c2: don't now prefer a previously-inapplicable ValueTask
due to digging in. [This test fails under the current proposal]
void c2(Func<Task<short>> lambda)
void c2(Func<ValueTask<byte>> lambda)
c2(async () => 3); // <-- When I upgrade, this should still pick the Task overload
TEST c3: don't introduce ambiguity errors about newly applicable candidates [conflicts with Test b7].
void c3(Func<Task<int>> lambda)
void c3(Func<ValueTask<int>> lambda)
c3(async () => 3); // <-- When I upgrade, this should still pick the Task overload
void c3g<T>(Func<Task<T>> lambda)
void c3g<T>(Func<ValueTask<T>> lambda)
c3g(async () => 3); // <-- When I upgrae, this should still pick the Task overload
void c3n(Func<Task> lambda)
void c3n(Func<ValueTask> lambda)
c3n(async () => {}); // <-- when I upgrade, this should still pick the Task overload
TEST c4: don't now prefer a previously-inapplicable ValueTask due to tie-breakers [conflicts with Test a5]. [This test fails under the current proposal]
void c4<T>(Func<T> lambda)
void c4<T>(Func<ValueTask<T>> lambda)
c4(async () => 3); // <-- When I upgrade, this should still pick the "T" overload
void c4n(Action lambda)
void c4n(Func<ValueTask> lambda)
c4n(async () => {}); // <-- When I upgrade, this should still pick the Action overload
TEST c5: don't now prefer a previously-inapplicable ValueTask due to more specific. [This test fails under the current proposal]
void c5<T>(Func<Task<T>> lambda)
void c5(Func<ValueTask<int>> lambda)
c5(async () => 3); // <-- When I upgrade, this should still pick the "Task<T>" overload
For explanation of why the proposal is this way, and to see alternatives, please read the [Design rationale and alternatives](feature - arbitrary async returns - discussion.md).