I was curious about that last part. How does it work? Let’s open up the dotPeek decompiler and find out!
Decompiling a Local Function
First, here’s a simple test program using a local function:
Admittedly, the above local function is not really needed in this case, but it’s simple enough code that the decompilation won’t be scary!
After decompiling the above program, we get the following for the AddFive method:
The above comments are helpfully added by the decompiler. As we can see, the C# compiler created the following for us:
Program.<AddFive>g__InnerAdd1_0
– this is our InnerAdd function, converted to a normal static function in the Program class. <AddFive> is simply part of the name, it’s not a generic type. Note that if the enclosing method is an instance method, the generated function will be an instance method.Program.<>c__DisplayClass1_0
– This is a generated class. It captures the a parameter, and is passed by reference into our function.
Two interesting things about this are that it only has one field, int32 a, that is used to pass our a parameter to the function, and that the class extends from System.ValueType. System.ValueType is the base class for all value types, so the generated value type will not cause heap allocations. The C# compiler prevents user code from extending System.ValueType.
Next, let’s look at the generated method:
Despite being a bit long, this is pretty straight-forward. It’s a static function that takes two parameters, int b and our generated obj1. It loads our argument obj1 onto the stack, then loads field obj1.a, then loads our argument b. Next, it calls add, which pops the top two values off the stack and adds them, then pushes the result back on the stack. Finally, it calls ret to return that result.
Decompiling a Local Function, with mutation!
Let’s make things a bit more interesting. What if our nested function mutates (gasp)?
The InnerAdd function is now a void function, that mutates a in the outer scope. In this case, our decompiled AddFive function looks like this:
This is more interesting than the first case. We can see that our generated class is set up ahead of time, then passed into the generated static function, and then all subsequent references to the parameter a are rewritten into references to the generated field! Fascinating!
Conclusion
Hopefully, this post has shown that there's no magic going on when we use C#7's local functions. With this knowledge, we can begin to reason about the runtime and performance characteristics of local functions, and understand the benefits of local functions over System.Func declarations.