Распределяет ли использование "new" в структуре его в куче или стеке?

Когда вы создаете экземпляр класса с new оператор, память выделяется в куче. Когда вы создаете экземпляр структуры с new Оператор, где память выделяется, в куче или в стеке?

8 ответов

Решение

Хорошо, давайте посмотрим, смогу ли я сделать это более понятным.

Во-первых, Эш прав: вопрос не в том, где расположены переменные типа значения. Это другой вопрос - и тот, на который ответ не просто "в стеке". Это сложнее, чем это (и стало еще сложнее в C# 2). У меня есть статья на эту тему, и я буду расширять ее, если потребуется, но давайте разберемся только с new оператор.

Во-вторых, все это действительно зависит от того, на каком уровне вы говорите. Я смотрю на то, что компилятор делает с исходным кодом, с точки зрения IL, который он создает. Более чем возможно, что JIT-компилятор будет делать умные вещи с точки зрения оптимизации большого количества "логического" распределения.

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

Наконец, все это только с текущей реализацией. Спецификация C# не определяет многое из этого - это фактически деталь реализации. Есть те, кто считает, что разработчикам управляемого кода на самом деле все равно. Я не уверен, что зайду так далеко, но стоит представить мир, где на самом деле все локальные переменные живут в куче - что все равно будет соответствовать спецификации.


Есть две разные ситуации с new оператор для типов значений: вы можете вызвать конструктор без параметров (например, new Guid()) или параметрический конструктор (например, new Guid(someString)). Они генерируют существенно разные IL. Чтобы понять почему, вам нужно сравнить спецификации C# и CLI: в соответствии с C# все типы значений имеют конструктор без параметров. Согласно спецификации CLI, типы значений не имеют конструкторов без параметров. (Получите конструкторы типа значения с отражением некоторое время - вы не найдете один без параметров.)

Для C# имеет смысл рассматривать "инициализацию значения с нулями" как конструктор, потому что он поддерживает согласованность языка - вы можете думать о new(...) как всегда, вызывая конструктор. Для CLI имеет смысл думать об этом по-другому, так как нет реального кода для вызова - и, конечно, нет кода для конкретного типа.

Также имеет значение, что вы собираетесь делать со значением после его инициализации. IL используется для

Guid localVariable = new Guid(someString);

отличается от IL, используемого для:

myInstanceOrStaticVariable = new Guid(someString);

Кроме того, если значение используется в качестве промежуточного значения, например, в качестве аргумента для вызова метода, все снова немного меняется. Чтобы показать все эти различия, вот небольшая тестовая программа. Это не показывает разницу между статическими переменными и переменными экземпляра: IL будет отличаться между stfld а также stsfld, но это все.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

Вот IL для класса, исключая нерелевантные биты (такие как nops):

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

Как видите, для вызова конструктора используется множество различных инструкций:

  • newobj: Распределяет значение в стеке, вызывает параметризованный конструктор. Используется для промежуточных значений, например, для присвоения полю или использования в качестве аргумента метода.
  • call instance: Использует уже выделенное место хранения (в стеке или нет). Это используется в приведенном выше коде для присвоения локальной переменной. Если одной и той же локальной переменной присваивается значение несколько раз с использованием нескольких new вызовов, он просто инициализирует данные поверх старого значения - он не выделяет больше места в стеке каждый раз.
  • initobj: Использует уже выделенное место хранения и просто стирает данные. Это используется для всех наших вызовов конструктора без параметров, включая те, которые присваиваются локальной переменной. Для вызова метода эффективно вводится промежуточная локальная переменная, а ее значение стирается initobj,

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

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

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

РЕДАКТИРОВАТЬ: Просто чтобы быть ясно, это верно только в некоторых случаях... в частности, значение guid не будет виден, если Guid Конструктор выдает исключение, поэтому компилятор C# может повторно использовать один и тот же слот стека. См. Сообщение Эрика Липперта в блоге о создании типа значения для получения дополнительной информации и случая, когда оно не применяется.

Я многому научился писать этот ответ - пожалуйста, попросите разъяснений, если что-то неясно!

Память, содержащая поля структуры, может быть выделена либо в стеке, либо в куче, в зависимости от обстоятельств. Если переменная типа структуры является локальной переменной или параметром, который не захвачен каким-либо анонимным делегатом или итератором, то он будет размещен в стеке. Если переменная является частью некоторого класса, то она будет размещена внутри класса в куче.

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

Аналогично для структур, размещенных в стеке, за исключением того, что C# требует, чтобы все локальные переменные были установлены в какое-то значение перед их использованием, поэтому вам нужно вызвать либо пользовательский конструктор, либо конструктор по умолчанию (конструктор, который не принимает параметров, всегда доступен для структуры).

Короче говоря, new - это неправильное выражение для структур, вызов new просто вызывает конструктор. Единственным местом хранения структуры является место, где она определена.

Если это переменная-член, она сохраняется непосредственно в том месте, в котором она определена, если это локальная переменная или параметр, она хранится в стеке.

Сравните это с классами, которые имеют ссылку, где бы структура не хранилась целиком, тогда как ссылки указывают где-то в куче. (Член внутри, локальный / параметр в стеке)

Это может помочь немного заглянуть в C++, где нет реального различия между классом / структурой. (В языке есть схожие имена, но они относятся только к доступности по умолчанию). Когда вы вызываете new, вы получаете указатель на расположение кучи, а если у вас есть ссылка без указателя, она сохраняется непосредственно в стеке или в другом объекте ала строит в C#.

Как и для всех типов значений, структуры всегда идут туда, где они были объявлены.

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

Редактировать: я mistankely ответил, что они ВСЕГДА идут в стек. Это неверно

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

Типы значений передаются по значению;) и поэтому не могут быть видоизменены в другой области видимости, чем те, в которых они определены. Чтобы иметь возможность изменять значение, вы должны добавить ключевое слово [ref].

Ссылочные типы передаются по ссылке и могут быть изменены.

Конечно, есть строки неизменяемых ссылочных типов, которые являются наиболее популярными.

Макет / инициализация массива: Типы значений -> нулевая память [имя,zip][имя, zip] Типы ссылок -> нулевая память -> null [ref][ref]

class или же struct Объявление похоже на план, который используется для создания экземпляров или объектов во время выполнения. Если вы определите class или же struct называется Person, Person - это имя типа. Если вы объявляете и инициализируете переменную p типа Person, p называется объектом или экземпляром Person. Можно создать несколько экземпляров одного и того же типа Person, и каждый экземпляр может иметь разные значения в своем properties а также fields,

class является ссылочным типом Когда объект class создается, переменная, которой назначен объект, содержит только ссылку на эту память. Когда ссылка на объект назначается новой переменной, новая переменная ссылается на исходный объект. Изменения, сделанные с помощью одной переменной, отражаются в другой переменной, поскольку они оба ссылаются на одни и те же данные.

struct это тип значения Когда struct переменная, к которой относится struct Назначено содержит фактические данные структуры. Когда struct присваивается новой переменной, она копируется. Поэтому новая переменная и исходная переменная содержат две отдельные копии одних и тех же данных. Изменения, внесенные в одну копию, не влияют на другую копию.

В общем, classes используются для моделирования более сложного поведения или данных, которые предназначены для изменения после class объект создан. Structs лучше всего подходят для небольших структур данных, которые содержат в основном данные, которые не предназначены для изменения после struct создано.

для большего...

Структуры распределяются по стеку. Вот полезное объяснение:

Структуры

Кроме того, классы при создании экземпляра в.NET выделяют память в куче или зарезервированном пространстве памяти.NET. Принимая во внимание, что структуры приводят к большей эффективности при реализации из-за распределения в стеке. Кроме того, следует отметить, что передаваемые параметры внутри структур делаются по значению.

В значительной степени структуры, которые считаются типами значений, размещаются в стеке, в то время как объекты распределяются в куче, а ссылка на объект (указатель) распределяется в стеке.

Другие вопросы по тегам