为委托分配本地function

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

public static Func 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 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 g__Triple0(int x) { return (this.factor * x); } } 

如果我将AssignLocalFunctionToDelegate()更改为

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

然后实施变为:

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

您可以看到它正在创建编译器生成的类的实例,以便与Console.WriteLine()一起使用。

你看不到的是它实际上分配3来反映反编译代码的factor 。 要看到这一点,你必须看看IL本身(这可能是我正在使用的反编译器中的失败,这是相当陈旧的)。

IL看起来像这样:

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

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

由于局部变量仅在调用方法时存在,

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

“寿命不长于方法激活” 不是局部变量的定义特征。 局部变量的定义特征是变量的名称仅对变量的局部范围内的代码有意义

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

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

但你已经知道:

 IEnumerable 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如何仍然具有值?

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

第一次调用BarAsync后, FooAsync返回任务。 但不知何故, sumn并且i继续使用值,即使在FooAsync返回给调用者之后FooAsync

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

不知何故,即使在MakeAdder返回后,他MakeAdder

在激活它们的方法返回后,局部变量可以很容易地继续存在; 这种情况一直在C#中发生。

这里发生了什么?

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

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

 return y=>Triple(y); 

如果您没有看到该lambda有任何问题,那么只需return Triple;就不会有任何问题return Triple; - 再次,这两个代码片段在逻辑上是相同的操作,所以如果有一个实现策略,那么另一个实现策略。

请注意,上述内容并不意味着需要编译器团队将本地方法生成为具有名称的lambdas。 编译器团队一如既往地可以自由选择他们喜欢的任何实现策略,具体取决于本地方法的使用方式。 正如编译器团队在根据lambda的细节生成lambda-to-delegate转换的策略中有许多微小的变化。

例如,如果您关心这些不同策略的性能影响,那么一如既往,没有任何东西可以替代尝试现实场景和进行经验测量。