13
votes

Why the C# 7 Compiler turns Local Functions into methods within the same class where their parent function is. While for Anonymous Methods (and Lambda Expressions) the compiler generates a nested class for each parent function, that will contain all of its Anonymous Methods as instance methods ?

For example, C# code (Anonymous Method):

internal class AnonymousMethod_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        Action act = delegate ()
        {
            Console.WriteLine(x);
        };
        act();
    }
}

Will produce IL Code (Anonymous Method) similar to:

.class private auto ansi beforefieldinit AnonymousMethod_Example
{
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0'
    {
        .field public int32 x

        .method assembly hidebysig instance void '<MyFunc>b__0' () cil managed 
        {
            ...
            AnonymousMethod_Example/'<>c__DisplayClass0_0'::x
            call void [mscorlib]System.Console::WriteLine(int32)
            ...
        }
        ...
    }
...

While this, C# code (Local Function):

internal class LocalFunction_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        void DoIt()
        {
            Console.WriteLine(x);
        };
        DoIt();
    }
}

Will generate IL Code (Local Function) similar to:

.class private auto ansi beforefieldinit LocalFunction_Example
{
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0' extends [mscorlib]System.ValueType
    {
        .field public int32 x
    }

    .method public hidebysig instance void MyFunc(string[] args) cil managed 
    {
        ...
        ldc.i4.5
        stfld int32 LocalFunction_Example/'<>c__DisplayClass1_0'::x
        ...
        call void LocalFunction_Example::'<MyFunc>g__DoIt1_0'(valuetype LocalFunction_Example/'<>c__DisplayClass1_0'&)
    }

    .method assembly hidebysig static void '<MyFunc>g__DoIt0_0'(valuetype LocalFunction_Example/'<>c__DisplayClass0_0'& '') cil managed 
    {
        ...
        LocalFunction_Example/'<>c__DisplayClass0_0'::x
        call void [mscorlib]System.Console::WriteLine(int32)
         ...
    }
}

Note that DoIt function has turned into a static function in the same class as its parent function. Also the enclosed variable x has turned into a field in a nested struct (not nested class as in the Anonymous Method example).

2
Try implementing a lambda the way that local methods are implemented and see what happens.Servy
@Servy would you please tell me how to do that ?KeyBored
You won't be able to. That's the point.Servy
@Servy this is exactly what I'm asking about. Why me (or the compiler guys) cannot implement the lambda the way that local methods are ?KeyBored
So try implementing a lambda using the same method that the local method uses, and see why it doesn't work.Servy

2 Answers

12
votes

Anonymous methods stored in delegates may be called by any code, even code written in different languages, compiled years before C# 7 came out, and the CIL generated by the compiler needs to be valid for all possible uses. This means in your case, at the CIL level, the method must take no parameters.

Local methods can only be called by the same C# project (from the containing method, to be more specific), so the same compiler that compiles the method will also be handled to compile all calls to it. Such compatibility concerns as for anonymous methods therefore don't exist. Any CIL that produces the same effects will work here, so it makes sense to go for what's most efficient. In this case, the re-write by the compiler to enable the use of a value type instead of a reference type prevents unnecessary allocations.

4
votes

The primary usage of anonymous methods (and lambda expressions) is the ability to pass them to a consuming method to specify a filter, predicate or whatever the method wants. They were not specifically suited for being called from the same method that defined them, and that ability was considered only later on, with the System.Action delegate.

On the other hand, local methods are the precise opposite - their primary purpose is to be called from the same method, like using a local variable.

Anonymous methods can be called from within the original method, but they were implemented in C# 2, and this specific usage wasn't taken into consideration.

So can local methods be passed to other methods, but their implementation details were designed in such a way that would be better for their purpose. After all, the difference you are observing is a simple optimisation. They could have optimised anonymous methods back in the day, but they didn't, and adding such optimisation now could potentially break existing programs (although we all know that relying on an implementation detail is a bad idea).

So let's see where the optimisation lies. The most important change is the struct instead of class. Well, an anonymous method needs a way to access the outside local variables even after the original method returns. This is called a closure, and the "DisplayClass" is what implements it. The main difference between C function pointers and C# delegates is that a delegate may optionally also carry a target object, simply used as this (the first argument internally). The method is bound to the target object, and the object is passed to the method every time the delegate is invoked (internally as the first argument, and the binding actually works even for static methods).

However, the target object is... well, object. You can bind a method to a value type, but it needs to be boxed before this. Now you can see why the DisplayClass needs to be a reference type in case of an anonymous method, because a value type will be a burden, not an optimisation.

Using a local method removes the need of binding a method to an object, and the consideration of passing the method to outside code. We can allocate the DisplayClass purely on the stack (as it should be for local data), presenting no burden on the GC. Now the developers had two choices - either make the LocalFunc instance and move it to the DisplayClass, or make it static and make the DisplayClass its first (ref) parameter. There is no difference in calling the method, so I think the choice was simply arbitrary. They could've decided otherwise, without any difference.

However, notice how quickly this optimisation is dropped once it could turn into a performance issue. A simple addition to your code, like Action a = DoIt; would immediately break the LocalFunc method. The implementation then immediately reverts to the one of the anonymous method, because the DisplayClass would need boxing etc.