为委托分配本地功能

Oli*_*bes 13 c# c#-7.0

在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
}
Run Code Online (Sandbox Code Playgroud)

它确实工作!

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

Eri*_*ert 18

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

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

"寿命不超过方法激活" 不是局部变量的定义特征.局部变量的定义特征是变量的名称仅对变量的局部范围内的代码有意义.

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

出于性能或正确性原因,局部变量的寿命可以任意延长或缩短.在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);
Run Code Online (Sandbox Code Playgroud)

如果本地人i和n的生命周期仅限于该方法,那么i和n在Numbers返回后如何仍然具有值?

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);
Run Code Online (Sandbox Code Playgroud)

FooAsync第一次调用后返回任务BarAsync.但不知何故sum,n并且i继续保持价值,即使在FooAsync返回呼叫者之后.

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

回来n之后,不知怎的MakeAdder.

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

这里发生了什么?

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

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

return y=>Triple(y);
Run Code Online (Sandbox Code Playgroud)

如果你没有看到lambda的任何问题,那么简单就不应该有任何问题return Triple;- 再次,这两个代码片段在逻辑上是相同的操作,所以如果有一个实现策略,那么就有一个实现对方的策略.

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

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

  • @ M.kazemAkhgary:它会*正确行为.通过分配闭包是否*实现*是一个实现细节.但是,是的,编译器团队非常聪明,如果不这样做就不会生成闭包. (2认同)

Mat*_*son 7

这是有效的,因为编译器创建了一个委托,它捕获factor闭包中的变量.

实际上,如果您使用反编译器,您将看到生成以下代码:

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

您可以看到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);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我换AssignLocalFunctionToDelegate()

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

然后实施变为:

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);
    };
}
Run Code Online (Sandbox Code Playgroud)

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

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

IL看起来像这样:

L_0009: ldc.i4.3 
L_000a: stfld int32 ConsoleApp3.Program/<>c__DisplayClass1_0::factor
Run Code Online (Sandbox Code Playgroud)

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