Local functions compared to lambda expressions

At first glance, local functions and lambda expressions are very similar. Depending on your needs, local functions may be a much better and simpler solution.

Let's examine the differences between the local function and lambda expression implementations of the factorial algorithm. First the version using a local function:

public static int LocalFunctionFactorial(int n)
{
    return nthFactorial(n);

    int nthFactorial(int number) => (number < 2) ? 
        1 : number * nthFactorial(number - 1);
}

Contrast that implementation with a version that uses lambda expressions:

public static int LambdaFactorial(int n)
{
    Func<int, int> nthFactorial = default(Func<int, int>);

    nthFactorial = (number) => (number < 2) ? 
        1 : number * nthFactorial(number - 1);

    return nthFactorial(n);
}

First, lambda expressions are implemented by instantiating a delegate and invoking that delegate. Local functions are implemented as method calls. The instantiation necessary for lambda expressions means extra memory allocations, which may be a performance factor in time critical code paths. Local functions do not incur this overhead. In the example above, the local functions version has 2 fewer allocations than the lambda expression version.

This recursive method is simple enough that the local function is implemented as a private method with a compiler generated name. Its only difference from other private methods is that it is semantically scoped inside the outer function.

Second, local functions can be called before they are defined. Lambda expressions must be declared before they are defined. This means local functions are easier to use in recursive algorithms, as shown above.

Notice that the version using the lambda expression must declare and initialize the lambda expression, nthFactorial before defining it. Not doing so results in a compile time error for referencing nthFactorial before assigning it. Recursive algorithms are easier to create using local functions.

Third, for lambda expressions, the compiler must always create an anonymous class and an instance of that class to store any variables captured by the closure. Consider this async example:

public Task<string> PerformLongRunningWorkLambda(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    Func<Task<string>> longRunningWorkImplementation = async () =>
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    };

    return longRunningWorkImplementation();
}

The closure for this lambda expression contains the address, index and name variables. In the case of local functions, the object that implements the closure may be a struct type. That would save on an allocation.

Note

The local function equivalent of this method also uses a class for the closure. Whether the closure for a local function is implemented as a class or a struct is an implementation detail. A local function may use a struct whereas a lambda will always use a class.

public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

One final advantage not demonstrated in this sample is that local functions can be implemented as iterators, using the yield return syntax to produce a sequence of values.

While local functions may seem redundant to lambda expressions, they actually serve different purposes and have different uses. Local functions are more efficient for the case when you want to write a function that is called only from the context of another method.