I was playing around with the ContinueWith
function and I ended up not understanding it.
In this example code:
var s = Task.FromResult(true).ContinueWith(async t => t).ContinueWith(async t => t);
await await await await await s;
What does each of the "awaits" await?
Let's answer in a very naive manner first:
var s = Task.FromResult(true).ContinueWith(async t => t ).ContinueWith(async t => t);
await await await await await s;
First, let's just split the expression into its constituent parts:
var s1 = Task.FromResult(true);
var s2 = s1.ContinueWith(async t => t );
var s3 = s2.ContinueWith(async t => t);
if we expand the two first var
declarations with full type, we get this:
Task<bool> s1 = Task.FromResult(true);
Task<Task<Task<bool>>> s2 = s1.ContinueWith(async t => t );
The first is simple, it is a task that will yield a boolean value. The next need some explanation.
When you do task.ContinueWith(...)
, you have an overload that takes in the task that was preceeding the ...
part, in this case, the task
reference. This task gets passed into the continuation method, so this:
s1.ContinueWith(async t => t)
means that s1
will be passed into the async t => t
continuation as t
, and then it will be returned.
Additionally, when you do:
var x = task.ContinueWith(...)
you get back a task that can be awaited, to get the return value from ...
. In this case it is another task because you just returned the task that was passed in.
So this means that this expression:
var s2 = s1.ContinueWith(async t => t );
will actually return a task, from ContinueWith, and this task wraps another task, from t => t
. So you can await the task from ContinueWith to get the s1
task, which can also be awaited.
Let's expand the expressions fully:
Task<bool> s1 = Task.FromResult(true);
Task<Task<Task<bool>>> s2 = s1.ContinueWith(async t => t );
Task<Task<Task<Task<Task<bool>>>>> s = s2.ContinueWith(async t => t);
each level adds another "task that wraps another task that wraps ...".
When we unwrap the 5-level await, we get this:
ContinueWith
async t = t
, so we await thisContinueWith
, so we await thisasync t = t
, so we await thisTask.FromResult(true)
so the final code can be looked at like this:
var s = Task.FromResult(true).ContinueWith(async t => t ).ContinueWith(async t => t);
^ ^ ^ ^ ^
| | | | |
await await await | await await s; |
| | | |
+--------------+ +-----------------------+
Note that all this happens synchronously, there is no waiting, no async code at all here. This is because all the tasks involved complete inline immediately, producing their results.
In general, ContinueWith is something best left for framework and library authors, and not really a construct we're meant to be using any more. There are many things you need to be aware of, in particular around exception handling, that async/await just does naturally for you without hassle.