C# 7.0 Local FunctionsC# 7.0 is coming! Even 6.0 is not released yet, you can already try new C# 7.0 features. To do that you need:

  • Visual Studio 15 preview
  • Set __DEMO__ and __DEMO_EXPERIMENTAL__ as Conditional compilation symbol in project settings.

Not all of the new features available in current preview, but you can already play with some of them.

But today we gonna look closer at

Local Functions

private static void Main(string[] args)
{
    int LocalFunction(int arg)
    {
        return 42 * arg;
    }

    Console.WriteLine(LocalFunction(1));
}

or shorter:

private static void Main(string[] args)
{
    int LocalFunction(int arg) => 42 * arg;

    Console.WriteLine(LocalFunction(1));
}

or even like that:

private static void Main(string[] args)
{
    int localVar = 42;
    int LocalFunction(int arg)
    {
        return localVar * arg;
    }

    Console.WriteLine(LocalFunction(1));
}

Do you like it? I do!

You can define local method in any scope and it will be available only in that scope (and in all inner scopes as well, the same as local variable).

You will have access to all outer scope's variables and methods (see below how it is implemented).

Benefits

  • Small helper methods required only in some scope (usually with LINQ expression)
  • No GC allocations comparing to anonymous methods and lambda expressions
  • You can pass ref and out parameters (comparing to anonymous methods and lambda expressions)

Let's look closer what is under the hood.

IL code

After disassembling first example with ildasm.exe I found following:

  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       12 (0xc)
    .maxstack  8
    IL_0000:  ldc.i4.1
    IL_0001:  call       int32 LocalFunctionsTest.Program::'<Main>g__LocalFunction0_0'(int32)
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_000b:  ret
  } // end of method Program::Main

  .method assembly hidebysig static int32 
          '<Main>g__LocalFunction0_0'(int32 arg) cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    // Code size       2 (0x2)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ret
  } // end of method Program::'<Main>g__LocalFunction0_0'

As you can see that LocalFunction is a regular instance method with specific name '<Main>g__LocalFunction0_0' and CompilerGenerated attribute. Here is C# equivalent:

private static void Main(string[] args)
{
    Console.WriteLine(LocalFunction(1));
}
        
private static int LocalFunction(int arg)
{
    return 1 * arg;
}

Because LocalFunction was declared in a static context, the method is also static. If you define it inside instance method it will also be comliped to instance method.

The third example (using outer scope variable) will show us a bit different IL:

.class public auto ansi beforefieldinit LocalFunctionsTest.Program
       extends [mscorlib]System.Object
{
  .class abstract auto ansi sealed nested private beforefieldinit '<>c__DisplayClass0_0'
         extends [mscorlib]System.ValueType
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    .field public int32 localVar
  } // end of class '<>c__DisplayClass0_0'

  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       31 (0x1f)
    .maxstack  2
    .locals init ([0] valuetype LocalFunctionsTest.Program/'<>c__DisplayClass0_0' 'CS$<>8__locals0')
    IL_0000:  ldloca.s   'CS$<>8__locals0'
    IL_0002:  initobj    LocalFunctionsTest.Program/'<>c__DisplayClass0_0'
    IL_0008:  ldloca.s   'CS$<>8__locals0'
    IL_000a:  ldc.i4.s   42
    IL_000c:  stfld      int32 LocalFunctionsTest.Program/'<>c__DisplayClass0_0'::localVar
    IL_0011:  ldc.i4.1
    IL_0012:  ldloca.s   'CS$<>8__locals0'
    IL_0014:  call       int32 LocalFunctionsTest.Program::'<Main>g__LocalFunction0_0'(int32,
                                                                                       valuetype LocalFunctionsTest.Program/'<>c__DisplayClass0_0'&)
    IL_0019:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_001e:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Program::.ctor

  .method assembly hidebysig static int32 
          '<Main>g__LocalFunction0_0'(int32 arg,
                                      valuetype LocalFunctionsTest.Program/'<>c__DisplayClass0_0'& A_1) cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
    // Code size       9 (0x9)
    .maxstack  8
    IL_0000:  ldarg.1
    IL_0001:  ldfld      int32 LocalFunctionsTest.Program/'<>c__DisplayClass0_0'::localVar
    IL_0006:  ldarg.0
    IL_0007:  mul
    IL_0008:  ret
  } // end of method Program::'<Main>g__LocalFunction0_0'

} // end of class LocalFunctionsTest.Program

As you can see, compiler created an inner struct and uses it to pass "outer scope" variable(s) to the local function as an implicit parameter. Here is C# equivalent:

public class Program
{
    private struct c__DisplayClass0_0
    {
        public int localVar;
    }

    private static void Main(string[] args)
    {
        int localVar = 42;
        var implicitArg = new c__DisplayClass0_0();
        implicitArg.localVar = localVar;

        Console.WriteLine(LocalFunction(1, ref implicitArg));
    }

    private static int LocalFunction(int arg, ref c__DisplayClass0_0 implicitArg)
    {
        return implicitArg.localVar * arg;
    }
}

That is why you cannot declare variables in local function which has the same name as a variable declared in outer scope.

In fact, it does not matter in what scope it was declared. It can be for loop, if, while, curly brackets {} or other local function. For each outer scope will be created a structure to hold all scope related variables.