将本地功能分配给代表

Assigning local functions to delegates

在 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
}

它确实有效!

我的问题是:为什么这样做有效?这是怎么回事?

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

事实上,如果你使用反编译器,你会看到生成了以下代码:

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

你可以看到 factor 将被捕获在一个闭包中。 (您可能已经知道,在幕后,编译器将生成一个 class,其中包含一个要保存 factor 的字段。)

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

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

您可以看到它正在创建编译器生成的 class 的实例以用于 Console.WriteLine()。

你看不到的是它在反编译代码中实际将3分配给factor的地方。要看到这一点,您必须查看 IL 本身(这可能是我使用的反编译器出现故障,它相当旧)。

IL 看起来像这样:

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

这是加载常量值 3 并将其存储在编译器生成的闭包 class.

factor 字段中

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

这个说法是错误的。一旦你相信一个错误的陈述,你的整个推理链就不再合理。

"Lifetime not longer than method activation" 是 而不是 局部变量的定义特征。 本地变量的定义特征是变量的名称仅对变量的本地范围内的代码有意义。

不要将作用域与生命周期混为一谈!它们不是同一件事。生命周期是一个运行时概念,描述了存储是如何被回收的。范围是一个编译时概念,描述名称如何与语言元素相关联。局部变量因其局部作用域而被称为局部变量;他们的位置是关于他们的名字,而不是他们的一生。

出于性能或正确性的原因,局部变量的生命周期可以任意延长或缩短。在 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);

如果locals i 和n 的生命周期仅限于方法,那么在Numbers returns 之后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 returns 第一次调用后的任务 BarAsync。但是不知何故 sumni 继续具有值,即使在 FooAsync returns 之后也是如此。

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

不知何故 n 即使在 MakeAdder 返回后仍然存在。

局部变量可以很容易地在激活它们的方法之后继续存在returns;这在 C# 中经常发生。

What is going on here?

转换为委托的局部函数在逻辑上与 lambda 没有太大区别;因为我们可以将 lambda 转换为委托,所以我们可以将本地方法转换为委托。

另一种思考方式:假设您的代码是:

return y=>Triple(y);

如果您没有发现该 lambda 有任何问题,那么 return Triple; 应该没有任何问题——同样,这两个代码片段在逻辑上是相同的操作,所以如果有一个的实施策略,然后是另一个的实施策略。

请注意,上述内容 而非 旨在暗示编译器团队 需要 将本地方法生成为带有名称的 lambda。编译器团队一如既往地可以根据本地方法的使用方式自由选择他们喜欢的任何实现策略。正如编译器团队根据 lambda 的细节在生成 lambda-to-delegate 转换的策略中有许多细微的变化一样。

例如,如果您关心这些不同策略的性能影响,那么一如既往,除了尝试现实场景和获得经验测量之外别无他法。