14

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).

KeyBored
  • 601
  • 4
  • 14
  • Try implementing a lambda the way that local methods are implemented and see what happens. – Servy Jul 26 '17 at 21:54
  • @Servy would you please tell me how to do that ? – KeyBored Jul 27 '17 at 00:28
  • You won't be able to. That's the *point*. – Servy Jul 27 '17 at 13:08
  • 1
    @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 Jul 27 '17 at 21:36
  • So try implementing a lambda using the same method that the local method uses, and see why it doesn't work. – Servy Jul 27 '17 at 21:36
  • @Servy Both anonymous and local methods can be called from any code. The CIL hasn't changed. But the purpose is different. Anonymous methods are made to be called from outside code, and local methods are made to be called from inside code, but that doesn't mean you cannot use them interchangeably. You can turn a local method into a delegate and pass it to any code you wish. You can make a delegate never leave the original method, and it will work like a local method. Doing so would just be pointless, but not impossible. – IS4 Jul 31 '17 at 10:11
  • @IllidanS4 You're talking about using the *C# code* for each operation interchangeably, which you are indeed able to do. What you're not able to do is use the CIL representations of those things interchangeably, or to implement one with the other. If you use a local method as a delegate *it will apply a transformation to it* in order for it to act like a lambda. If you just used the CIL for a local method as if it were a lambda *it wouldn't work*. – Servy Jul 31 '17 at 12:59
  • The accepted answer is still incorrect. – IS4 Jul 31 '22 at 11:39

2 Answers2

14

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.

  • 1
    Even if the delegate *could* be re-written to a method that accepts a parameter, *it wouldn't work*. It's not about being compatible with other systems or earlier code, it's that the problem *cannot be solved using that method* in the case of delegates. The two bits of code *do radically different things*. Since they do radically different things, their implementation is, understandably, different. – Servy Jul 26 '17 at 21:59
  • @Servy I'm not disagreeing, but I'm looking at it from a different point of view. The delegate *could* be re-written to a method that accepts a parameter of a different type, if the compiler could see and modify all invocations of the delegate. But of course there's no way the compiler can see and modify all invocations. –  Jul 26 '17 at 22:02
  • 1
    And even if it *could* modify all invocations you *still* wouldn't be able to solve the problem because the values that the method needs won't *exist* at many of those call sites to pass in, so the problems are deeper and more fundamental than that. – Servy Jul 26 '17 at 22:05
  • @Servy That part *is* possible even in the general case even with value types: it could be done with a static method bound to a boxed copy of the value type. The compiler could avoid the boxing when it was statically provable that the lifetime of the delegate did not exceed the method. But not worth the effort. –  Jul 26 '17 at 22:12
  • 1
    Now you're saying that the closure type created could be a value type, and yes, it could be, although it wouldn't be productive to do so, as you mentioned. That approach is still different from what the local method version is doing. – Servy Jul 26 '17 at 22:18
  • @Servy In what way? Both versions use a container type to hold the local variables. Both versions use a method that effectively takes a single argument to that container type. One happens to be the implicit `this` of an instance method, the other happens to be the first parameter of a static method. Other than that, what difference do you see that I'm missing? –  Jul 26 '17 at 22:30
  • @hvd Thanks for fixing the `string` typo.. It made me puzzled for a while :) – KeyBored Jul 27 '17 at 00:29
  • @hvd and what do you mean by _"Local methods can only be called by the same C# project"_ ? Is it possible to call a local function from outside its parent ?! .. If yes, would you please give me a link to an example. – KeyBored Jul 27 '17 at 00:48
  • @KeyBored No, that's not possible, and wasn't what I was trying to say. Is it clearer like this? –  Jul 27 '17 at 05:19
  • This answer is plainly wrong. Both anonymous methods and locally-defined methods can be passed to any code that accepts a delegate, no matter its original language. The implementation is different, but it makes no difference for the usage. – IS4 Nov 26 '20 at 14:47
4

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 local methods can 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 this way 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. 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... an object. You can bind a method to a value type, but it needs to be boxed prior to 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, requiring additional boxing.

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), or generally in the same place as the original local variables, presenting no burden on the GC. Now the developers had two choices ‒ either make the LocalFunc an instance method and move it to the DisplayClass, or make it static and make the DisplayClass its first (ref) parameter. There is no difference between those two options of 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 change the LocalFunc method. The result immediately reverts to the case of the anonymous method, because the DisplayClass would need boxing etc.

IS4
  • 11,945
  • 2
  • 47
  • 86
  • Have you got a reference for this? "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." – Enigmativity Jul 31 '22 at 11:22
  • @Enigmativity I do not have any special insight into the development process at the time of C# 2, but it follows from the usage. At the time, anonymous methods were primarily designed to be used alongside events, i.e. as `+= delegate`, seen from the choice of the `delegate` keyword and the possibility of ignoring any parameters. Using them in a different way required providing a delegate type, so even if the case of calling the delegate from the same method might have been considered, it did not have any effect on the way the code is generated. After all, you had to use a variable to store it. – IS4 Jul 31 '22 at 11:35
  • I think you're reaching to say something was considered or not. The C# design team are very thorough. – Enigmativity Jul 31 '22 at 23:32
  • Seems that local functions can also be used as predicates no? Saying they are the opposite seems misleading – Douglas Gaskell Jul 17 '23 at 21:41
  • @DouglasGaskell Do I? "So local methods can be passed to other methods", or are you referring to the word "local" in the sense that they are no longer local if they are exposed somewhere else? – IS4 Jul 18 '23 at 13:13