将本地函数分配给委托

14

在 C# 7.0 中,您可以声明局部函数,即嵌套在另一个方法中的函数。这些局部函数可以访问周围方法的局部变量。由于局部变量仅在调用方法时存在,因此我想知道是否可以将局部函数分配给委托(它可以比此方法调用更长时间存在)。

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
}

它确实有效!

我的问题是:为什么它有效?这里发生了什么?


1
我不能确定地回答,但我猜测这是因为它创建了一个闭包:C#中的Lambda表达式是否为闭包? - Dustin Hodges
可能是的。但我记不得看到过任何关于具体机制的规范或解释。 - Olivier Jacot-Descombes
也许这会有所帮助:闭包在幕后是如何工作的?(C#)。再次强调,它只讨论了C#中的闭包,而不是这个特定场景。 - Dustin Hodges
@DustinHodges:是的,但这适用于 lambda 表达式,而不完全适用于本地函数。 - Olivier Jacot-Descombes
@OlivierJacot-Descombes 为什么给本地方法命名会改变它的行为,而不是匿名的(除了你可以显式地通过名称引用它)。 - Servy
这不仅仅是给它一个名称。本地函数是一种可以直接调用的方法,而Lambda表达式则总是分配给代理或Expression<T>。请参见Local function vs Lambda C# 7.0 - Olivier Jacot-Descombes
2个回答

26

由于局部变量仅在方法被调用时存在,因此这种说法是错误的。一旦你相信了一个错误的陈述,你的整个推理链就不再完全正确。

"寿命不长于方法激活"并是局部变量的定义特征。一个局部变量的定义特征是该变量的名称仅对变量的本地范围内的代码有意义

不要混淆作用域和生命周期!它们不是同一个概念。 生命周期是运行时概念,描述如何回收存储器。 范围是编译时概念,描述名称如何与语言元素相关联。 局部变量之所以被称为本地变量,是因为它们具有本地作用域;它们的局部性与它们的名称有关,而不是它们的生命周期。

出于性能或正确性的原因,局部变量的生命周期可以任意延长或缩短。 C#中根本没有要求局部变量仅在方法被激活期间才具有生命周期。

但你已经知道这一点:

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);
如果本地变量i和n的生命周期仅限于该方法,那么在Numbers返回后,i和n如何仍然拥有值呢?
如果本地变量i和n的生命周期仅限于该方法,那么在Numbers返回后,i和n如何仍然拥有值呢?
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在第一次调用BarAsync后返回一个任务。但是,即使在FooAsync返回给调用方之后,sumni仍然具有值。

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

MakeAdder函数返回后,一些变量(包括n)可能会继续存在。

局部变量在其所属的方法返回之后很容易继续存在;在C#中这种情况时常发生。

这里到底发生了什么?

将局部函数转换为委托与将lambda表达式转换为委托在逻辑上并没有太大的区别;既然我们可以将lambda表达式转换为委托,那么同样也可以将局部方法转换为委托。

另一种思考方式是:假设你的代码如下:

return y=>Triple(y);
如果您对该lambda没有任何问题,那么仅仅 return Triple; 应该也不会有任何问题 -- 这两个代码片段在逻辑上是相同的操作,因此如果有一个实现策略,那么另一个也会有一个实现策略。
需要注意的是,上述内容并未意图暗示编译器团队必须像lambda一样生成具有名称的本地方法。编译器团队可以根据本地方法的使用情况自由选择任何实现策略,就像编译器团队根据lambda的细节有许多微小的变化来生成lambda-to-delegate转换策略一样。
例如,如果您关心这些各种策略的性能影响,那么像往常一样,没有任何替代品可以尝试真实场景并获得经验数据。

一个 lambda 表达式不是总是被分配给一个委托(或表达式)吗?而方法或本地函数可以自由存在? - Olivier Jacot-Descombes
@OlivierJacot-Descombes:是的,那又怎样?int Triple(int x) => factor * x;Func<int, int> Triple = (int x) => factor * x;在语义上基本相同。通常情况下,一些细节上的小差异并不重要。 - Eric Lippert
1
非常好的解释。但我现在有一个问题。当将本地函数分配给委托时,它就像一个lambda吗?我的意思是,如果我只在方法内部使用本地函数,它不会捕获闭包,对吗? - M.kazem Akhgary
2
@M.kazemAkhgary:它将会正确运行。无论这种行为是通过分配闭包来实现还是不是一个实现细节。但是,编译器团队非常聪明,当不必要时不生成闭包。 - Eric Lippert

11

这是因为编译器创建了一个在闭包中捕获factor变量的委托对象。

实际上,如果你使用反编译工具,你会看到以下代码被生成:

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

你可以看到factor将被捕获在闭包中 (你可能已经知道编译器会生成一个包含字段来保存factor的类.)

在我的机器上,它会创建以下类作为闭包:

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

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

如果我将AssignLocalFunctionToDelegate()更改为

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

那么实现就变成了:

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);
    };
}

你可以看到它创建了编译器生成的类的实例,并将其用于Console.WriteLine()。

但你无法在反编译后的代码中看到它实际上是在哪里给factor赋值为3。要查看这一点,你必须查看IL本身(这可能是我使用的反编译器的缺陷,该反编译器相当老)。

IL看起来像这样:

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

这将加载一个常量值 3 并将其存储在编译器生成的闭包类的 factor 字段中。


好的,这意味着本地函数实际上被内联到委托中。如果我们在AssignLocalFunctionToDelegate内部调用本地函数(除了返回它),它也会使用这个委托吗? - Olivier Jacot-Descombes
@OlivierJacot-Descombes 我已经在我的编辑答案中尝试回答了那个问题。 - Matthew Watson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接