(let's ignore the bug I've commented about... it happens only in Release mode)
The most important lesson here is that https://sharplab.io/ is the most useful tool for discovering how the code is compiled. You write some lines of code and then watch the decompiled code. You can the easily guess what the C# compiler is doing.
In general the C# compiler doesn't want to have two "places" for the same variable. It would be a nightmare to keep them synced. Let's make an example:
void Foo(int par, bool b, ref Action act)
{
if (b)
{
act = () => Console.WriteLine(par);
}
par = par + 1;
Console.WriteLine(par);
}
this is "translated" to:
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int par;
internal void <Foo>b__0()
{
Console.WriteLine(par);
}
}
private void Foo(int par, bool b, ref Action act)
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.par = par;
if (b)
{
act = new Action(<>c__DisplayClass0_.<Foo>b__0);
}
<>c__DisplayClass0_.par++;
Console.WriteLine(<>c__DisplayClass0_.par);
}
We can see that <>c__DisplayClass0_0
is instantiated at the beginning of the method and
it "captures" the par
variable. From the third line of the method onward, the par
variable isn't used anymore and instead <>c__DisplayClass0_.par
is used. Would it be possible to move the instantiation of <>c__DisplayClass0_
inside the if
? No, because moving it inside the if
wouldn't "capture" the changes done in the par++
:
private void Foo(int par, bool b, ref Action act)
{
if (b)
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.par = par;
act = new Action(<>c__DisplayClass0_.<Foo>b__0);
}
par++;
Console.WriteLine(par);
}
At the end of the method, par
and <>c__DisplayClass0_.par
would be different.
So in general a variable that must be closed must be put inside the hidden object supporting the closure before the first local function that uses it. To make it easier Roslyn simply put the variable inside the object at the moment of declaration:
void Foo(int par, bool b, ref Action act)
{
par = par + 1;
if (b)
{
act = () => Console.WriteLine(par);
}
par++;
Console.WriteLine(par);
}
is translated to
private void Foo(int par, bool b, ref Action act)
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.par = par;
<>c__DisplayClass0_.par++;
if (b)
{
act = new Action(<>c__DisplayClass0_.<Foo>b__0);
}
<>c__DisplayClass0_.par++;
Console.WriteLine(<>c__DisplayClass0_.par);
}
Here the first par++
could be done with the "real" par
(saving up the time for a dereferencing of <>c__DisplayClass0_
), and instead it is done to <>c__DisplayClass0_.par
.
By creating a local variable inside the if
, you use the fact that Roslyn will create/set up the hidden object when the variable is declared:
void Foo(int par, bool b, ref Action act)
{
if (b)
{
int par2 = par;
act = () => Console.WriteLine(par2);
}
par++;
Console.WriteLine(par);
}
is translated to
private void Foo(int par, bool b, ref Action act)
{
if (b)
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.par2 = par;
act = new Action(<>c__DisplayClass0_.<Foo>b__0);
}
par++;
Console.WriteLine(par);
}
Note that now par
and <>c__DisplayClass0_.par2
are distinct variables, and will have different values, but you as a programmer decided it, so it is ok.
The this
capture is different:
In the simplest case, when no other variables are captured, no hidden object is created:
int bar;
void Foo(int par, bool b, ref Action act)
{
if (b)
{
act = () => Console.WriteLine(bar);
}
Console.WriteLine(par);
}
is translated to:
private int bar;
private void Foo(int par, bool b, ref Action act)
{
if (b)
{
act = new Action(<Foo>b__1_0);
}
Console.WriteLine(par);
}
[CompilerGenerated]
private void <Foo>b__1_0()
{
Console.WriteLine(bar);
}
But if variables are captured, then an hidden object is created that must contain the this
:
int bar;
void Foo(int par, bool b, ref Action act)
{
if (b)
{
act = () => Console.WriteLine(par + bar);
}
Console.WriteLine(par);
}
is translated to:
[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
public int par;
public C <>4__this;
internal void <Foo>b__0()
{
Console.WriteLine(par + <>4__this.bar);
}
}
private int bar;
private void Foo(int par, bool b, ref Action act)
{
<>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
<>c__DisplayClass1_.par = par;
<>c__DisplayClass1_.<>4__this = this;
if (b)
{
act = new Action(<>c__DisplayClass1_.<Foo>b__0);
}
Console.WriteLine(<>c__DisplayClass1_.par);
}
Now, the delegate will contain a reference to this
(<>c__DisplayClass1_.<>4__this
), so the lifetime of this
will be >= the lifetime of the delegate (the GC can't collect the an instance of the class of Foo
until all the delegates generated by that instance.Foo
are collected).
Note that we could:
[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
public int par;
}
[CompilerGenerated]
private sealed class <>c__DisplayClass1_1
{
public int bar2;
public <>c__DisplayClass1_0 CS$<>8__locals1;
internal void <Foo>b__0()
{
Console.WriteLine(CS$<>8__locals1.par + bar2);
}
}
private int bar;
private void Foo(int par, bool b, ref Action act)
{
<>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
<>c__DisplayClass1_.par = par;
if (b)
{
<>c__DisplayClass1_1 <>c__DisplayClass1_2 = new <>c__DisplayClass1_1();
<>c__DisplayClass1_2.CS$<>8__locals1 = <>c__DisplayClass1_;
<>c__DisplayClass1_2.bar2 = bar;
act = new Action(<>c__DisplayClass1_2.<Foo>b__0);
}
Console.WriteLine(<>c__DisplayClass1_.par);
}
No this
captured here (but bar
and bar2
are now distinct!).
In the example given, they capture the local field _factory
probably for this reason. _factory
is a readonly field, so it won't change for the entire lifetime of the object, so they can close directly in it instead of closing around this
.
As a sidenote there is another problem with capturing variables that begin their lifetime at different nesting level of scope. ({ int a; { int b; } }
): Roslyn could create a multilevel hidden object (so multiple hidden objects that reference one another). It is seen in one of the last examples, where par
is inside <>c__DisplayClass1_0
while bar2
is inside <>c__DisplayClass1_1
.