是否在本地函数堆栈中声明了值类型变量?

问题描述 投票:2回答:1

我正在阅读最近介绍的本地功能,并开始怀疑这个问题。 lambda中值类型的Afaik局部变量在堆中分配。还有一些东西,当捕获值类型时,本地函数优于lambdas,在这种情况下不需要额外的堆分配。仍然跟着我不清楚:

  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?

c#
1个回答
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);
}

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

要了解原因,请考虑您的代码可以传递委托的事实 - 它可以将其存储在字段中,或者返回它,或者将其传递给另一个方法。在这种情况下,它不仅仅是由它的父节点同步调用(作为本地函数必须是),而是必须携带它用它捕获的变量。


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

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语句。


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

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刚刚被优化掉了(但是在<ParentFunction>g__LocalFunction|1()中已经被分配在堆栈中)。

© www.soinside.com 2019 - 2024. All rights reserved.