Assigning local functions to delegates

3.4k views Asked by At

In C# 7.0 you can declare local functions, i.e. functions living inside another method. These local functions can access local variables of the surrounding method. Since the local variables exist only while a method is being called, I wondered whether a local function could be assigned to a delegate (which can live longer than this method call).

public static Func<int,int> AssignLocalFunctionToDelegate()
{
    int factor;

    // Local function
    int Triple(int x) => factor * x;

    factor = 3;
    return Triple;
}

public static void CallTriple()
{
    var func = AssignLocalFunctionToDelegate();
    int result = func(10);
    Console.WriteLine(result); // ==> 30
}

It does in fact work!

My question is: why does this work? What is going on here?

2

There are 2 answers

2
Matthew Watson On BEST ANSWER

This works because the compiler creates a delegate which captures the factor variable in a closure.

In fact if you use a decompiler, you'll see that the following code is generated:

public static Func<int, int> AssignLocalFunctionToDelegate()
{
    int factor = 3;
    return delegate (int x) {
        return (factor * x);
    };
}

You can see that factor will be captured in a closure. (You are probably already aware that behind the scenes the compiler will generate a class that contains a field to hold factor.)

On my machine, it creates the following class to act as a closure:

[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
    // Fields
    public int factor;

    // Methods
    internal int <AssignLocalFunctionToDelegate>g__Triple0(int x)
    {
        return (this.factor * x);
    }
}

If I change AssignLocalFunctionToDelegate() to

public static Func<int, int> AssignLocalFunctionToDelegate()
{
    int factor;
    int Triple(int x) => factor * x;
    factor = 3;
    Console.WriteLine(Triple(2));
    return Triple;
}

then the implementation becomes:

public static Func<int, int> AssignLocalFunctionToDelegate()
{
    <>c__DisplayClass1_0 CS$<>8__locals0;
    int factor = 3;
    Console.WriteLine(CS$<>8__locals0.<AssignLocalFunctionToDelegate>g__Triple0(2));
    return delegate (int x) {
        return (factor * x);
    };
}

You can see that it is creating an instance of the compiler-generated class for use with the Console.WriteLine().

What you can't see is where it actually assigns 3 to factor in the decompiled code. To see that, you have to look at the IL itself (this may be a failing in the decompiler I'm using, which is fairly old).

The IL looks like this:

L_0009: ldc.i4.3 
L_000a: stfld int32 ConsoleApp3.Program/<>c__DisplayClass1_0::factor

That's loading a constant value of 3 and storing it in the factor field of the compiler-generated closure class.

4
Eric Lippert On

Since the local variables exist only while a method is being called,

This statement is false. And once you believe a false statement, your whole chain of reasoning is no longer sound.

"Lifetime not longer than method activation" is not a defining characteristic of local variables. The defining characteristic of a local variable is that the name of the variable is only meaningful to code in the local scope of the variable.

Do not conflate scope with lifetime! They are not the same thing. Lifetime is a runtime concept describing how storage is reclaimed. Scope is a compile-time concept describing how names are associated with language elements. Local variables are called locals because of their local scope; their locality is all about their names, not their lifetimes.

Local variables can have their lifetimes extended or shortened arbitrarily for performance or correctness reasons. There is no requirement whatsoever in C# that local variables only have lifetimes while the method is activated.

But you already knew that:

IEnumerable<int> Numbers(int n)
{
  for (int i = 0; i < n; i += 1) yield return i;
}
...
var nums = Numbers(7);
foreach(var num in nums)
  Console.WriteLine(num);

If the lifetime of locals i and n is limited to the method, then how can i and n still have values after Numbers returns?

Task<int> FooAsync(int n)
{
  int sum = 0;
  for(int i = 0; i < n; i += 1)
    sum += await BarAsync(i);
  return sum;
}
...
var task = FooAsync(7);

FooAsync returns a task after the first call to BarAsync. But somehow sum and n and i keep on having values, even after FooAsync returns to the caller.

Func<int, int> MakeAdder(int n)
{
  return x => x + n;
}
...
var add10 = MakeAdder(10);
Console.WriteLine(add10(20));

Somehow n sticks around even after MakeAdder returned.

Local variables can easily live on after the method which activated them returns; this happens all the time in C#.

What is going on here?

A local function converted to a delegate is logically not much different than a lambda; since we can convert lambdas to delegates, so to can we convert local methods to delegates.

Another way to think about it: suppose instead your code was:

return y=>Triple(y);

If you don't see any problem with that lambda, then there shouldn't be any problem with simply return Triple; -- again, those two code fragments are logically the same operation, so if there's an implementation strategy for one, then there is an implementation strategy for the other.

Note that the foregoing is not intending to imply that the compiler team is required to generate local methods as lambdas with names. The compiler team is, as always, free to choose whatever implementation strategy they like, depending on how the local method is used. Just as the compiler team has many minor variations in the strategy for generating a lambda-to-delegate conversion depending on the details of the lambda.

If, for instance, you care about the performance implications of these various strategies then as always there is no substitute for trying out realistic scenarios and getting empirical measurements.