C#7 - Local Functions Are Bringing Cleaner Code And Better Performance Comparing To Lambda Expressions
14 June 2017

TL;DR - Summary

Both Lambda Expression and Local Function will be converted by compiler to a "normal" function.

In case of lambda, new class will be created to hold captured variables. This class will also have a function doing all the work lambda expression was doing. Enclosing function will instantiate this class, create delegate and call this class function. So there is a heap overhead.

Local function will be converted to a static or member function depending on if this is used. All captured variables will be saved into a newly generated structure and passed as a reference to a created static function. Member function will be created only in case nothing is captured and if this is used (there is no need to create structure to hold anything). Conclusion, performance is better then lambdas.

Two more new things: Opposite from lambda, local function can yield and can be used before it is declared.

What is a Local Function ?

It is a function nested within another function and not visible outside of its enclosing function. Important feature is that it can "see" all local variables of its enclosing function and that leads us to the concept of closure.

C# Closures

While back I wrote article about closures in C# (You Should Know C# Closures). At that time all examples are made using lambda expressions. If you are already "lambda expert" you can skip first part of this article and jump right to the difference between lambdas and local functions. Only important things from the first part are that local function, unlike lambda, can yield and can be used before it is declared.

Why to nest functions at all ?

One obvious reason is to have some kind of local recursive helper function that is not polluting interface since it is hidden inside enclosing function. But there are two "smarter" usages.

Case 1: Early validation exception for function returning IEnumerable

Let's define two classes we will use in this example. Code could be written more optimal (using LINQ) but for the purpose of showing yield example it is dumbed-down.

public class Hero
{
public string Name { get; set; }
public int Version { get; set; }

public Hero(string name, int version)
{
Name = name;
Version = version;
}
}
public class City
{
private List<Hero> Heroes { get; }

public City()
{
Heroes = new List<Hero>();
}

public void AddHero(string name, int version)
{
Heroes.Add(new Hero(name, version));
}

public IEnumerable<Hero> GetHeroes(int minVersion)
{
int maxVersion = minVersion + 10;

int count = Heroes.Count();

if (count == 0) throw new InvalidOperationException("Not initialized.");

for (int i = 0; i < count; i++)
{
var hero = Heroes[i];

if(hero.Version >= minVersion && hero.Version <= maxVersion)
{
yield return hero;
}
}
}
}

Where is the problem with GetHeroes() implementation above ?
If we don't initialize Heroes validation exception will be "late". It will fire when we start iterating in foreach (location B below):

[Fact]
public void GetHeroesTest()
{
bool loopExecuted = false;

var sanFransokyo = new City();

// Comment this out to play with exception:
sanFransokyo.AddHero("Big Hero", 6);
sanFransokyo.AddHero("Small Hero", 5);

// * A *
// With Local Function you will get exception here:
var cityHeroes = sanFransokyo.GetHeroes(5);
// var cityHeroes = sanFransokyo.GetHeroesWithLocal(5);

// * B *
// Without Local Function you will get exception here:
foreach(var hero in cityHeroes)
{
Assert.True(hero.Version >= 5);
loopExecuted = true;
}

Assert.True(loopExecuted);
}

Let's write GetHeroesWithLocal() so we get validation error early (location A) before we start iterating:

public IEnumerable<Hero> GetHeroesWithLocal(int minVersion)
{
int maxVersion = minVersion + 10;

int count = Heroes.Count();

if (count == 0) throw new InvalidOperationException("Not initialized.");

return LoopThruHeroes();

// Local Function (declared after usage):
IEnumerable<Hero> LoopThruHeroes()
{
for (int i = 0; i < count; i++)
{
var hero = Heroes[i];

if(hero.Version >= minVersion && hero.Version <= maxVersion)
{
yield return hero;
}
}
}
}

Flip the comment in GetHeroesTest() example and use GetHeroesWithLocal(). Exception will fire early (location A) since yield is not part of the outer GetHeroesWithLocal() function. This pattern was used before, but helper function LoopThruHeroes() was defined as private member of the class. Now it is encapsulated where it belongs and also doesn't need to have any parameters. It is capturing minVersion and maxVersion from its enclosing function.

For those who are familiar with lambda expressions, two things that are new and different:

  1. Local function LoopThruHeroes() is declared after it is used. Lambda can't do that. Function declaration is not the same as execution so this is possible. Your code will look cleaner if you move helpers at the end.
  2. Local function can yield. Lambda can't.

For those not familiar with closure and capturing note that local function is using minVersion and maxVersion variables. Those variables are "captured" from its enclosing function. More details in "How it's made" section.

Case 2: Proper validation exception for async function

Let's add following async function to Hero class.

public async Task<int> IncreaseVersion(int versionIncrement)
{
if(versionIncrement < 1) throw new InvalidOperationException("Can't do that!");

await Task.Delay(200);

Version += versionIncrement;
return Version;
}

Using it like below, we will get stuck with a faulted Task at location B.

[Fact]
public async void IncreaseVersionTest()
{
var hero = new Hero("Big Hero", 6);

// To play with exception pass "-1" below:
var increase = -1;

// * A *
// With Local Function you will get exception here:
var taskToWait = hero.IncreaseVersion(increase);
// var taskToWait = hero.IncreaseVersionWithLocal(increase);

// * B *
// Without Local Function you will get stuck here with faulted task:
var newVersion = await taskToWait;

Assert.Equal(7, newVersion);
}

It would be ideal if we can get exception at location A and let our exception handling system to take care of it.
Let's write the same function using local helper function:

public Task<int> IncreaseVersionWithLocal(int versionIncrement)
{
if(versionIncrement < 1) throw new InvalidOperationException("Can't do that!");

return IncrementVersion();

async Task<int> IncrementVersion()
{
await Task.Delay(200);

Version += versionIncrement;
return Version;
}
}

Flip the comment in IncreaseVersionTest() to use IncreaseVersionWithLocal(). Exception will fire early (location A).

Definitely there are some good reasons to nest functions. Another one is to remove need to pass parameters since local function is capturing variables from the enclosing function. This will be more clear in the next section.

How it's made (Local Functions vs. Lambda Expressions)

In this section we will see how compiler transforms local functions and how this transformation is different from lambda expressions.

Let's start with "old" stuff and create two functions using lambda expressions. Both are capturing two local variables (b & c). First one is passing class member a as a parameter and second one is referring to it as this.a.

public class HowItIsMade
{
private int a = 1;

#region Lambda
public int SumWithLambda(int b)
{
int c = 3;

Func<int, int> LocalLambda = (x) => { return x + b + c; };

return LocalLambda(a);
}

public int SumWithLambdaAndThis(int b)
{
int c = 3;

Func<int> LocalLambdaWithThis = () => { return this.a + b + c; };

return LocalLambdaWithThis();
}
#endregion
}

To support first expression (LocalLambda()) compiler will generate class to hold captured variables and function to sum numbers:

[CompilerGenerated]
private sealed class \u003C\u003Ec__DisplayClass1_0
{
public int b;
public int c;

public \u003C\u003Ec__DisplayClass1_0()
{
base.\u002Ector();
}

internal int \u003CSumWithLambda\u003Eb__0(int x)
{
return x + this.b + this.c;
}
}

Also compiler will alter SumWithLambda() to use this new class by instantiating it, creating new delegate and executing it:

public int SumWithLambda(int b)
{
LocalFunctionsTest.HowItIsMade.\u003C\u003Ec__DisplayClass1_0 cDisplayClass10 = new LocalFunctionsTest.HowItIsMade.\u003C\u003Ec__DisplayClass1_0();
cDisplayClass10.b = b;
cDisplayClass10.c = 3;
// ISSUE: method pointer
return new Func<int, int>((object) cDisplayClass10, __methodptr(\u003CSumWithLambda\u003Eb__0))(this.a);
}

For the second expression (LocalLambdaWithThis()) that is using this.a, one member of the class will point to the entire HowItIsMade object:

[CompilerGenerated]
private sealed class \u003C\u003Ec__DisplayClass2_0
{
public int b;
public int c;
public LocalFunctionsTest.HowItIsMade \u003C\u003E4__this;

public \u003C\u003Ec__DisplayClass2_0()
{
base.\u002Ector();
}

internal int \u003CSumWithLambdaAndThis\u003Eb__0()
{
return this.\u003C\u003E4__this.a + this.b + this.c;
}
}

Lambda expression is converted to: delegate + class with members and function. And there is a bad news. We are paying penalty by pushing stuff on a heap!

Now let's see how local functions are doing better job with performance. Let's repeat both functions and this time use local functions instead of lambdas:

public int SumWithLocalFunction(int b)
{
int c = 3;

return LocalFunction(a);

int LocalFunction(int x)
{
return x + b + c;
};
}

public int SumWithLocalFunctionAndThis(int b)
{
int c = 3;

return LocalFunctionWithThis();

int LocalFunctionWithThis()
{
return this.a + b + c;
};
}

Compiler will convert LocalFunction() to a private static method:

[CompilerGenerated]
internal static int \u003CSumWithLocalFunction\u003Eg__LocalFunction3_0(int x, [In] ref LocalFunctionsTest.HowItIsMade.\u003C\u003Ec__DisplayClass3_0 obj1)
{
return x + obj1.b + obj1.c;
}

Captured variables are packed into a struct (and passed as a reference to a method above):

[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct \u003C\u003Ec__DisplayClass3_0
{
public int b;
public int c;
}

And SumWithLocalFunction() is altered to use this private method and structure:

public int SumWithLocalFunction(int b)
{
LocalFunctionsTest.HowItIsMade.\u003C\u003Ec__DisplayClass3_0 cDisplayClass30 = new LocalFunctionsTest.HowItIsMade.\u003C\u003Ec__DisplayClass3_0();
cDisplayClass30.b = b;
cDisplayClass30.c = 3;
return LocalFunctionsTest.HowItIsMade.\u003CSumWithLocalFunction\u003Eg__LocalFunction3_0(this.a, ref cDisplayClass30);
}

Much better then lambdas overhead! Structure is there just to save function signature of heaving multiple parameters (in case we capture multiple variables).

LocalFunctionWithThis() is also converted to static function even that is using this.a! Let's see when generated function will be static and when not. If local function captures variables, structure to hold those captured variables will be created anyway. Compiler will add to it one more property holding entire HowItIsMade object and generate static function to get and use this structure:

[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct \u003C\u003Ec__DisplayClass4_0
{
public int b;
public int c;
public LocalFunctionsTest.HowItIsMade \u003C\u003E4__this;
}

If local function doesn't capture local variables but refer to this, no structure will be created and compiler generated function will be instance member (not static).

Conclusion

Having nested functions totally make sense in some cases (please don't overdo it). In the past lambda expressions helped to overcome C# not having nested functions. Now with local functions we have better and cleaner solution.