How many times the code of a local function is created? Only once or anytime the parent function is called?

219 views Asked by At

Modern C# supported definition of nested functions. For example:

public bool Test(params int[] args) {
    bool isNumberValid(int i) {
        return i > 0;
   }
   foreach(var n in args) {
      if(!isNumberValid(n)) {
         return false;
      }
   }
   return true;
}

I wrote the above example only as a test case scenario and it doesn't matter if it can be refactored. Now the question is, how many times is the isNumberValid function created? Is it created only once in such a way that it is moved outside the parent function block by the compiler? or is it created over over at runtime anytime the parent function is invoked (scoped under the parent stack)?

2

There are 2 answers

4
Matthew Watson On BEST ANSWER

If you use a decompiler to inspect the output, you'll see something like this:

public bool Test(params int[] args)
{
    bool flag;
    int[] numArray = args;
    int num = 0;
    while (true)
    {
        if (num >= (int)numArray.Length)
        {
            flag = true;
            break;
        }
        else if (Program.<Test>g__isNumberValid|1_0(numArray[num]))
        {
            num++;
        }
        else
        {
            flag = false;
            break;
        }
    }
    return flag;
}

This shows that it has been compiled to a separate method, and is compiled only once.

Also note that for such a small local function in this example, the chances are that (for release mode builds) the JIT compiler will inline the function so that there isn't even a function call made for it.

0
AudioBubble On

Here is the generated MSIL code from ILSpy.

As we see, the local method is converted as being like any instance method of the current class and the code only exists once.

Thus the compiler "refactor" itself.

C# local methods are not like C and C ++ inline functions whose code is repeated according to usage.

Local methods allow to refactor the code and to create a more clean code while disallowing the call from the exterior of the method containing them, to segregate processings and/or to call them several times.

There is also previously available the local lambdas using Action and Func.

Also the delegate, being the ancestor of all.

Local functions (C# Programming Guide)

.method public hidebysig static bool Test (int32[] args) cil managed 
{
    .param [1]
        .custom instance void [mscorlib]System.ParamArrayAttribute::.ctor() = (01 00 00 00)
    .maxstack 2
    .locals init (
        [0] int32[],
        [1] int32,
        [2] int32 n,
        [3] bool,
        [4] bool
    )

    //  foreach (int i2 in args)
    //  {
    //      if (!isNumberValid(i2))
    //      {
    //          return false;
    //      }
    //  }
    IL_0003: ldarg.0
    // (no C# code)
    IL_0004: stloc.0
    IL_0005: ldc.i4.0
    IL_0006: stloc.1
    IL_0007: br.s IL_0026
    // loop start (head: IL_0026)
        IL_0009: ldloc.0
        IL_000a: ldloc.1
        IL_000b: ldelem.i4
        IL_000c: stloc.2
        // if (!isNumberValid(i2))
        IL_000e: ldloc.2
>>>>>>>>>>
        IL_000f: call bool ConsoleApp.Program::'<Test>g__isNumberValid|30_0'(int32)
>>>>>>>>>>
        IL_0014: ldc.i4.0
        IL_0015: ceq
        IL_0017: stloc.3
        IL_0018: ldloc.3
        // (no C# code)
        IL_0019: brfalse.s IL_0021

        // return false;
        IL_001c: ldc.i4.0
        IL_001d: stloc.s 4
        // (no C# code)
        IL_001f: br.s IL_0031

        IL_0022: ldloc.1
        IL_0023: ldc.i4.1
        IL_0024: add
        IL_0025: stloc.1

        IL_0026: ldloc.1
        IL_0027: ldloc.0
        IL_0028: ldlen
        IL_0029: conv.i4
        IL_002a: blt.s IL_0009
    // end loop

    // return true;
    IL_002c: ldc.i4.1
    IL_002d: stloc.s 4
    // (no C# code)
    IL_002f: br.s IL_0031

    IL_0031: ldloc.s 4
    IL_0033: ret
} // end of method Program::Test

Here is the local method now being a class method:

.method assembly hidebysig static bool '<Test>g__isNumberValid|30_0' (int32 i) cil managed 
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00)
    // Method begins at RVA 0x342c
    // Code size 10 (0xa)
    .maxstack 2
    .locals init (
        [0] bool
    )

    // return i > 0;
    IL_0001: ldarg.0
    IL_0002: ldc.i4.0
    IL_0003: cgt
    IL_0005: stloc.0
    // (no C# code)
    IL_0006: br.s IL_0008

    IL_0008: ldloc.0
    IL_0009: ret
} // end of method Program::'<Test>g__isNumberValid|30_0'