C# 7.0 Local Functions

By | April 6, 2016

Advertisement from Google

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)

Advertisement from Google

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.

10 thoughts on “C# 7.0 Local Functions

  1. Dennis Aries

    Good article.

    I’m not convinced by the about the quality of the feature. The feature makes it easy for developers to create local functions to perfom certain actions whereby the developer might create the same function multiple times (especially when working in larger teams). This will lead to larger applications with increasing unpredictable behaviour when some alteration has to be made.

    In order to use this feature, you’ll need a stronger grip on the way the team codes.

    Reply
  2. Alexei Omelaienko

    It’s a good article (IL disassembly is an especially nice touch).

    However, the feature itself is questionable. Not only does this feature duplicate previosly existing and easily accessible language functionality, it also essentially brings C# syntaxically even closer to JS. While I’m in no way a JS hater, those are very different languages, and constant evolutions from C# to JS is hardly a good thing, at the very least, aesthetically.

    And, of course, I support what Dennis wrote before.

    Reply
  3. Ody Mbegbu

    For those who fear that this feature for architectural reasons, remember that the same thing could be said about using Func and Action too. Any one can abuse any language feature without trying too hard. I really like this feature because it brings C# closer and closer to the functional paradigm which is very powerful. It would be easier to write functionally composed applications without the ungodly complexity of the Func scattered everywhere.

    PS- C# isn’t trying to become JS on the contrary its probably the other way round. C# is trying to become as a good as F# before its syntax becomes arcane like Java

    Reply
  4. Eberto Cepero

    Mmm, don’t like it. It goes against OO. You want utility functions? Make them private. So much nesting is confusing in the end.

    Reply
    1. Elrood

      C# is already a multiparadigm language. Going against OO is no argument at all. Aside from that, what rules of OO does it break?

      Any excessive nesting is confusing. That include class nesting, namespace nesting, whatever. If we would like to prevent all kinds of abuses I doubt we would end up with usable language. Besides too many private methods with calls restricted to one method are as confusing as nesting.

      Reply
  5. John Peters

    The closer C# gets to full blown functional support the better. I once was a skeptic on functional programming. But now can’t get enough of it. It’s changed my entire outlook.

    Reply
  6. Alexei Omelaienko

    @JohnPeters, there’s nothing wrong with functional programming, of course, but in the context of .NET platform there’s at least F# already for that. Bringing closer two languages from different paradigms, even should they be best in their own paradigm, would result in a single language which is equally bad for everything (e.g. Java).

    Reply
    1. Elrood

      Which paradigm you are talking about? Object Oriented, declarative or functional? C# 4.0 already has elements from all of them.
      Beside at that moment mixing C# with F# requires two different projects. It would be way easier and natural if you could do both in the same project depending on what is appropriate for current module/problem/section/whatever. And that is what C# is IMHO going for.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *