Когда вы создаете экземпляр класса с new
оператором, память выделяется в куче. Когда вы создаете экземпляр структуры с new
оператором, где выделяется память, в куче или в стеке?
Когда вы создаете экземпляр класса с new
оператором, память выделяется в куче. Когда вы создаете экземпляр структуры с new
оператором, где выделяется память, в куче или в стеке?
Ответы:
Хорошо, давайте посмотрим, смогу ли я сделать это более понятным.
Во-первых, Эш прав: вопрос не в том, где расположены переменные типа значения . Это другой вопрос, на который ответ не просто «в стеке». Это сложнее, чем это (и стало еще сложнее в 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 # может повторно использовать тот же слот стека. См. Сообщение Эрика Липперта в блоге о создании типа значения для получения дополнительной информации и случая, когда оно не применяется.
Я многому научился писать этот ответ - пожалуйста, попросите разъяснений, если что-то неясно!
List<Guid>
и добавить эти 3 к нему? Это было бы 3 распределения (тот же IL)? Но они хранятся где-то волшебно
guid
имеет только наполовину перезаписаны, так как он не будет виден в любом случае.
Память, содержащая поля структуры, может быть выделена либо в стеке, либо в куче, в зависимости от обстоятельств. Если переменная типа структуры является локальной переменной или параметром, который не захвачен каким-либо анонимным классом делегата или итератора, то он будет размещен в стеке. Если переменная является частью некоторого класса, то она будет размещена внутри класса в куче.
Если структура размещена в куче, то для выделения памяти фактически не нужно вызывать оператор new. Единственная цель - установить значения полей в соответствии с тем, что находится в конструкторе. Если конструктор не вызывается, то все поля получат значения по умолчанию (0 или ноль).
Точно так же для структур, размещенных в стеке, за исключением того, что C # требует, чтобы все локальные переменные были установлены в какое-то значение перед их использованием, поэтому вы должны вызвать либо пользовательский конструктор, либо конструктор по умолчанию (конструктор, который не принимает параметров, всегда доступен структуры).
Короче говоря, new - это неправильное выражение для структур, вызов new просто вызывает конструктор. Единственным местом хранения структуры является место, где она определена.
Если это переменная-член, она сохраняется непосредственно в том месте, в котором она определена, если это локальная переменная или параметр, она хранится в стеке.
Сравните это с классами, которые имеют ссылку, где бы структура не хранилась целиком, тогда как ссылки указывают где-то в куче. (Член внутри, локальный / параметр в стеке)
Это может помочь немного заглянуть в C ++, где нет реального различия между классом / структурой. (Есть похожие имена в языке, но они относятся только к доступности по умолчанию вещей). Когда вы вызываете new, вы получаете указатель на местоположение кучи, в то время как если у вас есть ссылка без указателя, она сохраняется непосредственно в стеке или в другом объекте ала строит в C #.
Как и для всех типов значений, структуры всегда идут туда, где они были объявлены .
Смотрите этот вопрос здесь для более подробной информации о том, когда использовать структуры. И этот вопрос здесь для дополнительной информации о структурах.
Редактировать: я ответил, что они ВСЕГДА идут в стек. Это неправильно .
Я, наверное, что-то здесь упускаю, но почему мы заботимся о распределении?
Типы значений передаются по значению;) и поэтому не могут быть видоизменены в другой области видимости, чем те, в которых они определены. Чтобы иметь возможность изменять значение, вы должны добавить ключевое слово [ref].
Ссылочные типы передаются по ссылке и могут быть изменены.
Конечно, есть строки неизменяемых ссылочных типов, которые являются наиболее популярными.
Расположение / инициализация массива: Типы значений -> нулевая память [имя, zip] [имя, zip] Типы ссылок -> нулевая память -> null [ref] [ref]
Объявление class
or struct
похоже на план, который используется для создания экземпляров или объектов во время выполнения. Если вы определяете class
или struct
вызываете Person, Person - это имя типа. Если вы объявляете и инициализируете переменную p типа Person, p называется объектом или экземпляром Person. Можно создать несколько экземпляров одного и того же типа Person, и каждый экземпляр может иметь разные значения в своих properties
и fields
.
А class
является ссылочным типом. Когда объект class
создается, переменная, которой назначен объект, содержит только ссылку на эту память. Когда ссылка на объект назначается новой переменной, новая переменная ссылается на исходный объект. Изменения, сделанные с помощью одной переменной, отражаются в другой переменной, поскольку оба они ссылаются на одни и те же данные.
А struct
является типом значения. Когда создается a struct
, переменная, которой struct
назначен, содержит фактические данные структуры. Когда struct
объект назначен новой переменной, он копируется. Поэтому новая переменная и исходная переменная содержат две отдельные копии одних и тех же данных. Изменения, внесенные в одну копию, не влияют на другую копию.
Обычно classes
используются для моделирования более сложного поведения или данных, которые предназначены для изменения после создания class
объекта. Structs
лучше всего подходят для небольших структур данных, которые содержат в основном данные, которые не предназначены для изменения после struct
создания.
В значительной степени структуры, которые считаются типами значений, размещаются в стеке, в то время как объекты распределяются в куче, а ссылка на объект (указатель) распределяется в стеке.
Структуры распределяются по стеку. Вот полезное объяснение:
Кроме того, классы при создании экземпляра в .NET выделяют память в куче или зарезервированном пространстве памяти .NET. Принимая во внимание, что структуры приводят к большей эффективности при реализации из-за распределения в стеке. Кроме того, следует отметить, что передаваемые параметры внутри структур делаются по значению.