是否在堆栈分配的局部函数中声明了值类型变量?

时间:2019-04-18 10:33:38

标签: c#

我正在阅读最近介绍的局部函数,并开始对这个问题感到疑惑。在Lambda中,值类型的Afaik局部变量在堆中分配。另外,在捕获值类型时,局部函数比lambda更具优势,在这种情况下,不需要额外的堆分配。我仍然不清楚以下内容:

  1. 是否在堆栈上分配的局部函数中声明了局部值类型变量?
  2. 在“父”函数中声明并在局部函数中捕获的值类型变量呢?

(前提是父母本身不是匿名的)。

编辑:

int ParentFunction ()
{
    int parentVarLambda = 0;
    int parentVarLocal = 0;

    Func<int> lamdaFuncion = () => parentVarLambda + 1;

    int a = lamdaFuncion();
    int b = LocalFunction();

    return a + b;

    int LocalFunction()
    {
        int localFuncVar = 1;
        return parentVarLocal += localFuncVar ;
    }
}

在哪里分配parentVarLambda,parentVarLocal和localFuncVar?

1 个答案:

答案 0 :(得分:5)

除非进行其他操作,否则都不分配堆(特别是如果编译器无法保证本地函数捕获的变量的生存期不超过父方法的生存期,例如委托是指本地函数,或者本地函数包含yield returnawait语句)。

假设您拥有:

public void M(int i) {
    Inner(i + 1);

    void Inner(int x)
    {
        int j = x + i;
        Console.WriteLine(j);   
    }
}

使用精彩的SharpLab,我们可以看到它被编译为:

[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <>c__DisplayClass0_0
{
    public int i;
}

public void M(int i)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = default(<>c__DisplayClass0_0);
    <>c__DisplayClass0_.i = i;
    <M>g__Inner|0_0(<>c__DisplayClass0_.i + 1, ref <>c__DisplayClass0_);
}

[CompilerGenerated]
internal static void <M>g__Inner|0_0(int x, ref <>c__DisplayClass0_0 P_1)
{
    Console.WriteLine(x + P_1.i);
}

因此,编译器采用了我们的内部函数,并将其重写为静态方法。内部函数的参数保留为静态方法的参数。内部函数捕获的内容最终作为编译器生成的结构上的字段,由ref传递(以避免复制,从而使在静态方法中对其进行的更改反映在调用方法中)。

在该内部函数中分配的结构将以静态方法(即在堆栈上)进行相同的分配。


现在让我们将其与等效代码进行比较,但使用委托:

public void M(int i) {
    Action<int> inner = x =>
    {
        int j = x + i;
        Console.WriteLine(j);   
    };

    inner(i + 1);
}

gets compiled to

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int i;

    internal void <M>b__0(int x)
    {
        Console.WriteLine(x + i);
    }
}

public void M(int i)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.i = i;
    new Action<int>(<>c__DisplayClass0_.<M>b__0)(<>c__DisplayClass0_.i + 1);
}

在这里我们可以看到区别。编译器生成了一个新的 class ,该类具有用于保存委托捕获的变量的字段,并具有一个包含委托主体的方法。它必须使用一个类,而不是通过引用传递的结构。

要了解为什么,请考虑一下您的代码可以传递委托的事实-它可以将其存储在字段中,或将其返回,或将其传递给另一个方法。在这种情况下,它不仅会被其父对象同步调用(必须像本地函数一样),而且还必须随身携带它捕获的变量。


请注意,如果我们创建一个引用本地函数的委托,则会发生类似的事情:

public void M(int i) {
    void Inner(int x)
    {
        int j = x + i;
        Console.WriteLine(j);   
    }

    Action<int> inner = Inner;
    inner(i + 1);
}

gets compiled to the same as before

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int i;

    internal void <M>g__Inner|0(int x)
    {
        Console.WriteLine(x + i);
    }
}

public void M(int i)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.i = i;
    new Action<int>(<>c__DisplayClass0_.<M>g__Inner|0)(<>c__DisplayClass0_.i + 1);
}

在这里,编译器发现仍然需要创建委托,因此它生成的代码与上一个示例相同。

请注意,在其他情况下,编译器在调用本地函数时也必须执行堆分配,例如,由于本地函数包含yield returnawait语句而必须是可恢复的。 / p>


要解决您编辑中的特定示例:

int ParentFunction ()
{
    int parentVarLambda = 0;
    int parentVarLocal = 0;

    Func<int> lamdaFuncion = () => parentVarLambda + 1;

    int a = lamdaFuncion();
    int b = LocalFunction();

    return a + b;

    int LocalFunction()
    {
        int localVar = 1;
        return parentVarLocal += localVar;
    }
}

我们可以再次put this into SharpLab, and get

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int parentVarLambda;

    public int parentVarLocal;

    internal int <ParentFunction>b__0()
    {
        return parentVarLambda + 1;
    }

    internal int <ParentFunction>g__LocalFunction|1()
    {
        int num = 1;
        return parentVarLocal += num;
    }
}

private int ParentFunction()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.parentVarLambda = 0;
    <>c__DisplayClass0_.parentVarLocal = 0;
    int num = new Func<int>(<>c__DisplayClass0_.<ParentFunction>b__0)();
    int num2 = <>c__DisplayClass0_.<ParentFunction>g__LocalFunction|1();
    return num + num2;
}

请注意,编译器意识到必须为委托 创建一个生成类的新实例,因此,它只是选择以相同的方式处理本地函数而无需额外费用。在这种情况下并没有太大区别,但是在委托和本地函数捕获相同变量的情况下需要此技术-需要将它们提升到同一生成的类中。

因此,parentVarLambdaparentVarLocal都分配在同一个编译器生成的类上,并且localFuncVar刚刚被优化掉了(但是会在{ {1}}。