Являются ли переменные типа значения, объявленные в локальном стеке функций, распределенными?

#c#

#c#

Вопрос:

Я читал о недавно введенных локальных функциях и начал задаваться этим вопросом. Afaik локальные переменные типов значений в лямбдах распределяются в куче. Также было кое-что, что локальные функции имеют преимущество перед лямбдами при захвате типов значений, которые в этом случае не требуют дополнительного выделения кучи. Все еще следующее мне непонятно:

  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. @jdweng: Это совсем не так.

2. «Afaik локальные переменные типов значений в лямбдах распределяются в куче» — нет, это не так. Локальные переменные, которые фиксируются лямбда-выражениями, должны быть распределены по куче, поскольку они сохраняются при нескольких вызовах делегатов, но это не то же самое, что локальные переменные, объявленные в лямбда-выражениях. Если бы вы могли привести более конкретные примеры того, что вас интересует (в виде кода), вам будет легче помочь.

3. @jdweng: «Все объекты среды выполнения должны быть выделены в стек» Нет, на самом деле это не так.

4. @jdweng: Тогда вы должны знать, что это а) неточно; б) не так, как кто-либо другой использует этот термин, и, следовательно, бесполезно при переполнении стека.

5. @jdweng: это не стек — в куче нет операций «push» и «pop». Это не терминология Microsoft — это простая информатика. (Я бы согласился, что это также не обычная куча компьютерных наук, но использование кучи там также не зависит от MS.) И даже если бы это было технически правильно самым сложным способом, это, очевидно , не то, о чем говорит OP: они говорят о распределении кучи против распределения стека. Почему бы не попытаться общаться с людьми в терминах, которые на самом деле используют все?

Ответ №1:

Ничто из этого не выделяется в куче, если не происходит что-то еще (особенно, если компилятор не может гарантировать, что время жизни переменных, захваченных локальной функцией, не превышает время жизни родительского метода, например, если делегат ссылается на локальную функцию или локальная функция содержит yield return await операторы or ).

Допустим, у вас есть:

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

Итак, компилятор взял нашу внутреннюю функцию и переписал ее как статический метод. Параметры внутренней функции остаются параметрами статического метода. Вещи, захваченные внутренней функцией, в конечном итоге становятся полями в сгенерированной компилятором структуре, которая передается по ссылке (чтобы избежать копирования и чтобы изменения, внесенные в нее в статическом методе, отражались в вызывающем методе).

Структуры, выделенные в этой внутренней функции, будут просто распределены одинаково в статическом методе, то есть в стеке.


Теперь давайте сравним это с эквивалентным кодом, но с использованием делегата:

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

    inner(i   1);
}
 

Это компилируется в:

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

Это компилируется так же, как и раньше:

 [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 return await операторы or .


Для рассмотрения конкретного примера в вашем редактировании:

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

Мы можем снова поместить это в SharpLab и получить:

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

Обратите внимание, что компилятор понял, что ему все равно нужно создать новый экземпляр сгенерированного класса для делегата, поэтому он просто решил работать с локальной функцией таким же образом без каких-либо дополнительных затрат. В данном случае это не имеет большого значения, но этот метод необходим, когда делегат и локальная функция фиксируют одни и те же переменные — их нужно перенести в один и тот же сгенерированный класс.

Из-за этого оба parentVarLambda и parentVarLocal были выделены в одном и том же классе, сгенерированном компилятором, и localFuncVar просто были оптимизированы (но были бы выделены в стеке в <ParentFunction>g__LocalFunction|1() ).

Комментарии:

1. За исключением того, что это не всегда так. Попробуйте добавить Action<int> action = Inner; в качестве другой строки вашего метода…

2. @JonSkeet Хорошая мысль, спасибо! Я все равно собирался сопоставить локальные функции с кодом, сгенерированным для делегатов, поэтому имеет смысл связать их вместе в конце.

3. Это также происходит в других случаях, когда компилятор не может контролировать время жизни, например, если это итератор или асинхронный метод.

4. Да, комментарий на этот счет находится в конце 3-го раздела (скрытый в шуме, предоставлен). Я повторю это в начале.