14

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?

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • 1
    I can't answer definitively but I'm guessing it is because it creates a closure: [Are Lambda expressions in C# closures?](http://stackoverflow.com/questions/9591476/are-lambda-expressions-in-c-sharp-closures) – Dustin Hodges Dec 13 '16 at 20:34
  • Probably yes. But I can't remember having seen any specification or explanation of the exact mechanism. – Olivier Jacot-Descombes Dec 13 '16 at 20:44
  • Maybe this will help: [How do closures work behind the scenes? (C#)](http://stackoverflow.com/a/1928654/76512). Again it is only talking about closures in c# not this particular scenario – Dustin Hodges Dec 13 '16 at 20:56
  • @DustinHodges: Yes but this applied to lambdas which is not quite the same as local functions. – Olivier Jacot-Descombes Dec 13 '16 at 21:07
  • @OlivierJacot-Descombes Why would giving a name to the local method change it's behavior over it being anonymous (other than that you can refer to it by name, obviously). – Servy Dec 13 '16 at 21:14
  • It's not just giviing it a name. A local function is a method that cn be called as is, where as a lambda expression is always assigned to a delegate or an `Expression`. SEE [Local function vs Lambda C# 7.0](http://stackoverflow.com/q/40943117/880990) – Olivier Jacot-Descombes Dec 13 '16 at 21:18

2 Answers2

26

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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Isn't a lambda always assigned to a delegate (or Expression), where as a method or a local function can live freely? – Olivier Jacot-Descombes Dec 13 '16 at 22:45
  • @OlivierJacot-Descombes: Yep. So what? `int Triple(int x) => factor * x;` and `Func Triple = (int x) => factor * x;` are basically the same thing semantically. There are a few differences in minor details that don't typically matter. – Eric Lippert Dec 14 '16 at 02:02
  • 1
    very nice explanation. but I have one question now. local function is like a lambda when its assigned to delegates right? I mean if I only use local function within the method, its not going to capture closures or is it? – M.kazem Akhgary Feb 20 '18 at 06:43
  • 2
    @M.kazemAkhgary: It will *behave* correctly. Whether that behavior is *implemented* by allocating a closure or not is an implementation detail. But yes, the compiler team is pretty smart about not generating closures when doing so is unnecessary. – Eric Lippert Oct 15 '18 at 11:56
11

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.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Ok, this means that the local function is in fact inlined into the delegate. If we called the local function inside `AssignLocalFunctionToDelegate` (in addition to returning it) would it use this delegate as well? – Olivier Jacot-Descombes Dec 13 '16 at 21:06
  • @OlivierJacot-Descombes I've tried to answer that question in my edited answer. – Matthew Watson Dec 13 '16 at 21:13