In this article, we’ll discuss the differences between local functions and lambda expressions in C#. 

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Lambda Expressions

Lambda expressions are anonymous functions. They are short blocks of code that accept parameters and return a value. We can assign the lambda expression to a variable or pass it as a parameter. We define lambda expression using => operator. On the left side are parameters, and on the right side is an expression body:

var lambda = (int x) => x * x;
Console.WriteLine(lambda(3));

Here, our lambda expression accepts the integer variable x as an input parameter. In its body, it calculates the square of the input parameter value. We store the lambda expression in the lambda variable. Next, we output the result of the call to the lambda expression with argument value 3.  

If you’re new to Lambda Expression, and want to learn more about the lambda expressions, read our article Lambda Expressions in C#.

Local Functions

Local functions are methods that are nested in other methods. We can call local functions only from their containing method:

void PrintSquare(int x)
{
    Console.WriteLine(Square(3));

    int Square(int x)
    {
      return x * x;
    }
}

The Square() local function is nested in the PrintSquare() method. It accepts the integer variable x and returns the square number of the input parameter value.

Local functions can improve your code readability if used wisely, if you want to explore local functions further, read our article Local Functions in C#.

Local Functions vs Lambda Expressions Performance Considerations

When we can use either a local function or lambda expression, there is a good reason to decide on a local function, which is performance.

Let’s create a benchmark test using a simple example of calculating the square of an integer: 

[MemoryDiagnoser]
public class LocalFunctionLambdaExpressionBenchmark
{
    [Benchmark]
    public void SquareAsLambda()
    {
        var lambda = (int x) => x * x;

        lambda(3);
    }

    [Benchmark]
    public void SquareAsLocal()
    {
        Square(3);

        int Square(int x)
        {
            return x * x;
        }
    }
}

In the benchmark, we decorate SquareAsLambda() and SquareAsLocal() methods with the [Benchmark] attribute to enable running it. Both methods calculate the square of the input number, but the first implements lambda expression, and the second implements local function.

To learn more about benchmarking, read our article Introduction to Benchmarking in C# and ASP.NET Core Projects.

Now, let’s take a look at the benchmark results:

| Method         | Mean      | Error     | StdDev    | Median    | Allocated |
|--------------- |----------:|----------:|----------:|----------:|----------:|
| SquareAsLocal  | 0.0446 ns | 0.0343 ns | 0.0653 ns | 0.0043 ns |         - |
| SquareAsLambda | 0.2715 ns | 0.0403 ns | 0.1033 ns | 0.2271 ns |         - |

The local function outperforms the lambda expression for a significant time. 

The compiler creates delegates for lambda expressions, which require object instantiation. Local functions do not require delegate allocation unless explicitly captured by a delegate. Additionally, lambda expressions usually capture a local variable into a class. On the other hand, local functions can use a struct. Finally, the compiler inlines the local functions. 

Lambda expressions always require heap allocation. Local functions can avoid heap allocation. For them, heap allocation is necessary only when we convert them to a delegate, or some variables the local functions capture are also captured by lambda expressions or local functions that are converted to a delegate.

All these differences contribute to better performance of local functions than lambda expressions.

Recursive Functions

Both local functions and lambda expressions can be recursive. But local functions are naturally recursive, while lambda expressions require some workaround:

public class Recursivity
{
    public static int FactorialAsLocalFunction(int input)
    {
        int Factorial(int n)
        {
            if (n <= 1) return 1;

            return n * Factorial(n - 1);
        }

        int result = Factorial(input);

        return result;
    }

    public static int FactorialAsLambdaExpression(int input)
    {
        Func<int, int> factorial = null!;

        factorial = (int n) =>
        {
            if (n <= 1) return 1;

            return n * factorial(n - 1);
        };

        int result = factorial(input);

        return result;
    }
}

We implement the calculation of the factorial of the input integer variable. The FactorialAsLocalFunction() method calculates the factorial using the local function and the FactorialAsLambdaExpression() method uses a lambda expression.

The FactorialAsLocalFunction() method’s code using a local function is simple, as local functions can directly call themselves. The FactorialAsLambdaExpression() method with a lambda expression requires declaring the factorial delegate variable first and then assign the lambda expression to enable the lambda to reference itself. 

Generic Local Functions

Unlike lambda expressions, local functions can be generic

public static class GenericLocalFunctions
{
    public static void SwapElements(ref object left, ref object right)
    {
        static void Swap<T>(ref T left, ref T right)
        {
            T temp = left;
            left = right;
            right = temp;
        }

        Swap(ref left, ref right);
    }
}

Here, we implement the Swap() local function, which can exchange two objects in a generic way.

This is not possible with lambda expression. We can’t do something like:

public static void SwapElements(ref object left, ref object right)
{
    Func<T> swap = (T left, T right) =>
    {
        T temp = left;
        left = right;
        right = temp;
    };

    swap(ref left, ref right);
}

In this case, we end up with compiler error. 

Iterator Local Functions

We can implement local functions as iterators. They can use yield return to produce values sequence or yield break to stop iteration: 

public static IEnumerable<int> IntegersToAbsoluteValue(IEnumerable<int> input)
{
    return AbsoluteValueIterator();

    IEnumerable<int> AbsoluteValueIterator()
    {
        foreach (var number in input)
        {
            yield return Math.Abs(number);
        }
    }
}

Here, we accept a list of integers and return an iterator that returns their absolute values. These values will be calculated only when the enumeration is materialized.

Such an implementation is not possible with lambda expressions, as they don’t allow a yield return statement.

Other Differences Between Local Functions and Lambda Expressions

Local functions have named parameters that are visible on the caller side. We define lambda expressions as delegates and they don’t expose parameter names on the caller’s side. Besides that, local functions have names, while lambda expressions are anonymous, and we must assign them to a variable first.

We can define local functions anywhere inside the owning method. We can define them before or after we call them. Local functions can even be after the return statement. On the other hand, we must define lambda expressions before we use them. 

Local Functions vs Lambda Expressions Features

Let’s summarize the differences between lambda expression and local functions:

FeatureLocal FunctionsLambda Expressions
PerformanceFasterSlower
RecursivityYes, nativeYes, with workaround
GenericYesNo
IteratorYesNo
Named parametersYesNo
Order mattersNoYes

Conclusion

There are some cases when we must use lambda expressions. These include passing the function as a delegate and using LINQ or a similar API that requires a delegate. Sometimes, we may prefer more concise syntax for inline functions. 

Conversely, when we want to use methods as iterators or generic functions, we have to use local functions, as lambda expressions do not have these capabilities. 

When we have a choice, local functions are better performance-wise and have more straightforward code. Even when we can use either, such as in recursive functions, we consider local functions more readable and easier to understand. The same is true when considering parameters’ names or return types. 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!