Распределяет ли использование «new» в структуре его в куче или стеке? - PullRequest
276 голосов
/ 15 октября 2008

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

Ответы [ 8 ]

297 голосов
/ 15 октября 2008

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

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

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

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

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

<Ч />

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

В 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 # может использовать один и тот же слот стека. См. Блог Эрика Липперта о конструкции типа значения для получения более подробной информации и случая, когда не не применимо.

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

38 голосов
/ 15 октября 2008

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

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

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

11 голосов
/ 15 октября 2008

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

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

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

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

5 голосов
/ 15 октября 2008

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

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

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

4 голосов
/ 01 января 2009

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

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

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

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

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

2 голосов
/ 08 сентября 2012
Объявление

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

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

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

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

для более ...

1 голос
/ 15 октября 2008

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

1 голос
/ 15 октября 2008

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

* 1004 Структуры *

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...