So what’s actually happening here when you create an ‘anonymous function’ is that the compiler creates an implicit class nested in the class you’ve written.
That class is used as the ‘state’ of the anonymous function.
Any variables you access in the anonymous function are actually stored as a class member field of this implicit nested class.
This way when it’s called as a callback, it’s modifying the same class field on an referenced object in the heap rather than on the stack by value.
We can see this here:
using System;
namespace Console01
{
class ExampleCallback
{
public void DoWork()
{
float someValue = 0f;
IndirectModify((v) => someValue += v);
Console.WriteLine(someValue);
}
private void IndirectModify(System.Action<float> callback)
{
callback(5f);
}
}
}
Now lests see what the IL (the intermediate language C# is compiled into for the mono/.net runtime to process… it’s not exactly human friendly readable):
// Type: Console01.ExampleCallback
// Assembly: Console01, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: D88AEA8D-32B1-48F4-88AB-8A9AB2DC7DE1
// Location: C:\Users\lordo\Documents\Visual Studio 2015\Projects\Console01\Console01\bin\Debug\Console01.exe
// Sequence point data from C:\Users\lordo\Documents\Visual Studio 2015\Projects\Console01\Console01\bin\Debug\Console01.pdb
.class private auto ansi beforefieldinit
Console01.ExampleCallback
extends [mscorlib]System.Object
{
.class nested private sealed auto ansi beforefieldinit
'<>c__DisplayClass0_0'
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.field public float32 someValue
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method '<>c__DisplayClass0_0'::.ctor
.method assembly hidebysig instance void
'<DoWork>b__0'(
float32 v
) cil managed
{
.maxstack 8
// [15 35 - 15 49]
IL_0000: ldarg.0 // this
IL_0001: ldarg.0 // this
IL_0002: ldfld float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
IL_0007: ldarg.1 // v
IL_0008: add
IL_0009: stfld float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
IL_000e: ret
} // end of method '<>c__DisplayClass0_0'::'<DoWork>b__0'
} // end of class '<>c__DisplayClass0_0'
.method public hidebysig instance void
DoWork() cil managed
{
.maxstack 3
.locals init (
[0] class Console01.ExampleCallback/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
)
IL_0000: newobj instance void Console01.ExampleCallback/'<>c__DisplayClass0_0'::.ctor()
IL_0005: stloc.0 // 'CS$<>8__locals0'
// [13 9 - 13 10]
IL_0006: nop
// [14 13 - 14 34]
IL_0007: ldloc.0 // 'CS$<>8__locals0'
IL_0008: ldc.r4 0.0
IL_000d: stfld float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
// [15 13 - 15 51]
IL_0012: ldarg.0 // this
IL_0013: ldloc.0 // 'CS$<>8__locals0'
IL_0014: ldftn instance void Console01.ExampleCallback/'<>c__DisplayClass0_0'::'<DoWork>b__0'(float32)
IL_001a: newobj instance void class [mscorlib]System.Action`1<float32>::.ctor(object, native int)
IL_001f: call instance void Console01.ExampleCallback::IndirectModify(class [mscorlib]System.Action`1<float32>)
IL_0024: nop
// [16 13 - 16 42]
IL_0025: ldloc.0 // 'CS$<>8__locals0'
IL_0026: ldfld float32 Console01.ExampleCallback/'<>c__DisplayClass0_0'::someValue
IL_002b: call void [mscorlib]System.Console::WriteLine(float32)
IL_0030: nop
// [17 9 - 17 10]
IL_0031: ret
} // end of method ExampleCallback::smile:oWork
.method private hidebysig instance void
IndirectModify(
class [mscorlib]System.Action`1<float32> callback
) cil managed
{
.maxstack 8
// [20 9 - 20 10]
IL_0000: nop
// [21 13 - 21 26]
IL_0001: ldarg.1 // callback
IL_0002: ldc.r4 5
IL_0007: callvirt instance void class [mscorlib]System.Action`1<float32>::Invoke(!0/*float32*/)
IL_000c: nop
// [22 9 - 22 10]
IL_000d: ret
} // end of method ExampleCallback::IndirectModify
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method ExampleCallback::.ctor
} // end of class Console01.ExampleCallback
Note how the very first declaration inside our ExampleCallback class is this:
.class nested private sealed auto ansi beforefieldinit
'<>c__DisplayClass0_0'
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.field public float32 someValue
A nested class named this weird gobbily goope ‘<>c__DisplayClass0_0’ with a float32 field named ‘someValue’.
Back in our C# code that’s the variable in the DoWork method.
This ‘<>c__DisplayClass0_0’ class is instantiated in our DoWork method, and that field is what actually gets modified.
We could manually do this ourselves in plain C# like so:
using System;
namespace Console01
{
class ExampleCallbackUnwrapped
{
private class DoWorkStateObject
{
public float someValue;
public void Invoke(float v)
{
someValue += v;
}
}
public void DoWork()
{
DoWorkStateObject obj = new DoWorkStateObject();
obj.someValue = 0f;
IndirectModify(obj.Invoke);
Console.WriteLine(obj.someValue);
}
private void IndirectModify(System.Action<float> callback)
{
callback(5f);
}
}
}
So basically… our value type is getting boxed and referenced via this weird nested class.