Local Functions in C# – to capture or not to capture when passing parameters down?

Local functions in C# are clever in terms of their capturing – at least in the Roslyn implementation. When the compiler is able to guarantee that you aren’t creating a delegate from the local function (or doing something else that will prolong the lifetime of the variable) it can use a ref parameter with all the captured variables in a generated struct to communicate with the local function. For example, your second method would end up as something like:

public int MultiplyFoo(int id)
{
    __MultiplyFoo__Variables variables = new __MultiplyFoo__Variables();
    variables.id = id;
    return __Generated__LocalBar(ref variables);
}

private struct __MultiplyFoo__Variables
{
    public int id;
}

private int __Generated__LocalBar(ref __MultiplyFoo__Variables variables)
{
    return variables.id * 2;
}

So there’s no heap allocation required as there would be for (say) a lambda expression converted to a delegate. On the other hand, there is the construction of the struct and then copying the values into that. Whether passing an int by value is more or less efficient than passing the struct by reference is unlikely to be significant… although I guess in cases where you had a huge struct as a local variable, it would mean that using implicit capture would be more efficient than using a simple value parameter. (Likewise if your local function used lots of captured local variables.)

The situation already gets more complicated when you have multiple local variables being captured by different local functions – and even more so when some of those are local functions within loops etc. Exploring with ildasm or Reflector etc can be quite entertaining.

As soon as you start doing anything complicated, like writing async methods, iterator blocks, lambda expressions within the local functions, using method group conversions to create a delegate from the local function etc… at that point I would hesitate to continue guessing. You could either try to benchmark the code each way, or look at the IL, or just write whichever code is simpler and rely on your bigger performance validation tests (which you already have, right? 🙂 to let you know if it’s a problem.

Leave a Comment