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.
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):
Let's write GetHeroesWithLocal()
so we get validation error early (location A) before we start iterating:
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:
- 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. - 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.
Using it like below, we will get stuck with a faulted Task
at location B.
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:
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
.
}
To support first expression (LocalLambda()
) compiler will generate class to hold captured variables and function to sum numbers:
Also compiler will alter SumWithLambda()
to use this new class by instantiating it, creating new delegate and executing it:
For the second expression (LocalLambdaWithThis()
) that is using this.a
, one member of the class will point to the entire HowItIsMade
object:
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:
Compiler will convert LocalFunction()
to a private static
method:
Captured variables are packed into a struct
(and passed as a reference to a method above):
And SumWithLocalFunction()
is altered to use this private method and structure:
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:
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.