Использует ли "новое" в структуре выделение в куче или стеке?

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

Ответ 1

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

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

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

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

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


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

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

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

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 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, но на самом деле (для этого конкретного кода) стек распределяется только один раз, а затем одно и то же хранилище местоположение повторно используется.

EDIT: просто, чтобы быть ясным, это верно только в некоторых случаях... в частности, значение guid не будет видно, если конструктор guid генерирует исключение, поэтому компилятор С# может повторно использовать один и тот же слот стека. См. Eric Lippert сообщение в блоге о построении типа значения для получения более подробной информации и случая, когда оно не применяется.

Я многому научился в написании этого ответа - просьба уточнить, если кто-либо из них неясен!

Ответ 2

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

Изменить: Я ответил, что они ВСЕГДА идут в стек. Это неверно.

Ответ 5

Вероятно, мне что-то не хватает, но почему мы заботимся о распределении?

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

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

Существуют, конечно, неизменные ссылочные типы, которые являются самыми популярными.

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

Ответ 6

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

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

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

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

подробнее...

Ответ 7

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

Ответ 8

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

Структуры

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