While walking the dog I was thinking about Action<T>, Func<T>, Task<T>, async/await (yes, nerdy, I know...) and constructed a little test program in my mind and wondered what the answer would be. I noticed I was unsure about the result, so I created two simple tests.
Here's the setup:
What would the output be? The initial value, or the changed value?
A little surprising but understandable, the output is the changed value. My explanation: the variable is not pushed onto the stack until the action executes, so it will be the changed one.
public class foo
{
string token;
public foo ()
{
this.token = "Initial Value";
}
void DoIt(string someString)
{
Console.WriteLine("SomeString is '{0}'", someString);
}
public void Run()
{
Action op = () => DoIt(this.token);
this.token = "Changed value";
// Will output "Changed value".
op();
}
}
Next, I created a variation:
public class foo
{
string token;
public foo ()
{
this.token = "Initial Value";
}
Task DoIt(string someString)
{
// Delay(0) is just there to try if timing is the issue here - can also try Delay(1000) or whatever.
return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", someString));
}
async Task Execute(Func<Task> op)
{
await op();
}
public async void Run()
{
var op = DoIt(this.token);
this.token = "Changed value";
// The output will be "Initial Value"!
await Execute(() => op);
}
}
Here I made DoIt() return a Task. op is now a Task and no longer an Action. The Execute() method awaits the task. To my surprise, the output is now "Initial Value".
Why does it behave differently?
DoIt() won't be executed until Execute() gets called, so why does it capture the initial value of token?
Complete tests: https://gist.github.com/Krumelur/c20cb3d3b4c44134311f and https://gist.github.com/Krumelur/3f93afb50b02fba6a7c8
You have a couple of misconceptions here. Firstly, when you call DoIt, it returns a Task that has already begun execution. Execution doesn't start only when you await the Task.
You also create a closure over the someString variable, the value of which does not change when you reassign the class-level field:
Task DoIt(string someString)
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", someString));
}
The Action passed to ContinueWith closes on the someString variable. Remember that strings are immutable so, when you reassign the value of token, you are actually assigning a new string reference. The local variable someString inside DoIt, however, retains the old reference, so its value remains the same even after the class field is reassigned.
You could solve this problem by instead having this action close over the class-level field directly:
Task DoIt()
{
return Task.Delay(0).ContinueWith(t
=> Console.WriteLine("SomeString is '{0}'", this.token));
}
In both cases, you are making a closure. However, you're making a closure over different things in the two cases.
In the first case, you're making an anonymous method with a closure over this - when you finally execute the delegate, it will take the current value of this, get the current value of this.token and use that. So you see the modified value.
In the second case, there is no closure over this - or if it is, it doesn't make a difference. You pass this.token explicitly, and the DoIt method only needs to make a closure over its own argument, someString. This happens immediately (synchronously), rather than lazily - so the initial value of this.token is captured. await doesn't actually execute the delegate - it only waits for the results of the asynchronous method. The method itself has already ran, and only it's asynchronous part is, well, asynchronous - in this case, only the Console.WriteLine("SomeString is '{0}'", someString).
If you want to see this more clearly, add Thread.Sleep(1000) after this.token = "Changed value"; - you will see SomeString is 'Initial Value' printed out before you even get to the await.
To make the second example behave like the first, all you need to do is change op to be a delegate again, rather than a Task - var op = () => DoIt(this.token);. This delays execution of DoIt again, and causes the same closure as in the first example.
TL;DR:
The behaviour is different because in the first case, you defer the execution of DoIt(this.token), while in the second example you run DoIt(this.token) immediately. The other points in my answer are also important, but this is the crux.
Let's break down each case.
Starting with the Action<T>:
My explanation: the variable is not pushed onto the stack until the action executes, so it will be the changed one
This hasn't anything to do with the stack. The compiler generates the following from your first code snippet:
public foo()
{
this.token = "Initial Value";
}
private void DoIt(string someString)
{
Console.WriteLine("SomeString is '{0}'", someString);
}
public void Run()
{
Action action = new Action(this.<Run>b__3_0);
this.token = "Changed value";
action();
}
[CompilerGenerated]
private void <Run>b__3_0()
{
this.DoIt(this.token);
}
The compiler emits a named method from your lambda expression. Once you invoke the action, and since we're in the same class, the this.token is the updated "Changed Value". The compiler doesn't even lift this into a display class, since this is all created and invoked inside the instance method.
Now, for the async method. There are two state-machines being generated, ill skimp the bloat of the state-machine and get to the relevant parts. The state-machine does the following:
this.<>8__1 = new foo.<>c__DisplayClass4_0();
this.<>8__1.op = this.<>4__this.DoIt(this.<>4__this.token);
this.<>4__this.token = "Changed value";
taskAwaiter = this.<>4__this.Execute(new Func<Task>(this.<>8__1.<Run>b__0)).GetAwaiter();
What happens here? token is passed to DoIt, which will return a Func<Task>. That delegate contains a reference to the old token string, "Initial Value". Remember, even though we're talking about reference types, they are all passed by value. This effectively means that there is a new storage location for the old string now in the DoIt method which points to "Initial Value". Then, the next line changes token to "Changed Value". The string stored inside the Func and the one that was changed are now pointing at two different strings.
When you invoke the delegate, it will print the initial value, as the op task stores your older, stale value. That is why you're seeing two different behaviors.