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





Как и все типы значений, структуры всегда идут туда, где они были объявлен.
См. Этот вопрос здесь для получения дополнительной информации о том, когда использовать структуры. И этот вопрос здесь для получения дополнительной информации о структурах.
Редактировать: Я ошибочно ответил, что они ВСЕГДА идут в стек. Это неверный.
Нет, я думаю, что это точно правильно - хотя это не то же самое, что ссылочный тип. Значение переменной живет там, где оно объявлено. Значение переменной ссылочного типа - это ссылка, а не фактические данные, вот и все.
Таким образом, всякий раз, когда вы создаете (объявляете) тип значения в любом месте метода, он всегда создается в стеке.
@Ash (недавний): ложь; если переменная "захвачена" (в лямбда / анон-метод), тогда это фактически поле в классе, созданном компилятором, то есть в куче
Джон, ты упустил мою точку зрения. Причина, по которой этот вопрос был задан впервые, заключается в том, что многим разработчикам (включая меня, пока я не прочитал CLR Via C#) непонятно, где выделяется структура, если вы используете оператор new для ее создания. Сказать, что «структуры всегда идут туда, где они были объявлены» не является однозначным ответом.
@Ash - также «блоки итератора» - для разработчика они выглядят как методы, но опять же, все переменные становятся полями, так что куча.
@Marc, технически ты конечно прав, почему мне вспоминается этот анекдот? jokes2go.com/jokes/6342.html
Эш: Да, вы должны понимать, что новый оператор в C# отличается от C++. Нет, это не значит, что правильно утверждать, что он всегда основан на стеке.
Лучше, чем дать неправильный, но полезный ответ по-видимому. На самом деле, проблема стека и кучи связана с множеством последствий с захваченными переменными и блоками итератора - то есть без них он не работал бы. Итак, если вы считаете, что эти полезности ...
Да, и что касается вашего «что неясно для разработчиков»: я видел, как многие люди были сбиты с толку утверждением, что типы значений всегда выделяются в стеке. Мне приходилось исправлять этот миф огромное количество раз в группах новостей.
Эш: Для ясности, я понимаю, почему вы проводите различие между «где живут структуры» и «что делает новый (...)» - но я думаю, что вопрос требует тщательного ответа очень, чтобы избежать путаницы. Есть много укромных мест (например, вызов конструктора без параметров ...)
@Jon, Справедливо, это хорошее обсуждение, которое помогло мне, и я уверен, что поможет и другим.
@Ash: Если у меня будет время, я постараюсь написать ответ, когда доберусь до работы. Впрочем, это слишком большая тема, чтобы пытаться осветить ее в поезде :)
Структуры выделяются в стек. Вот полезное объяснение:
Additionally, classes when instantiated within .NET allocate memory on the heap or .NET's reserved memory space. Whereas structs yield more efficiency when instantiated due to allocation on the stack. Furthermore, it should be noted that passing parameters within structs are done so by value.
Это не распространяется на случай, когда структура является частью класса - в этот момент она находится в куче вместе с остальными данными объекта.
Да, но на самом деле он фокусируется на задаваемом вопросе и отвечает на него. Проголосовал.
... при этом все еще неверный и вводящий в заблуждение. Извините, но на этот вопрос нет коротких ответов - Джеффри - единственный полный ответ.
Память, содержащая поля структуры, может быть выделена либо в стеке, либо в куче, в зависимости от обстоятельств. Если переменная типа структуры является локальной переменной или параметром, который не захватывается каким-либо анонимным делегатом или классом итератора, то она будет размещена в стеке. Если переменная является частью какого-либо класса, она будет размещена внутри класса в куче.
Если структура выделяется в куче, то вызов оператора new на самом деле не требуется для выделения памяти. Единственная цель - установить значения полей в соответствии с тем, что находится в конструкторе. Если конструктор не вызывается, все поля получат значения по умолчанию (0 или null).
Аналогично для структур, размещенных в стеке, за исключением того, что C# требует, чтобы все локальные переменные были установлены на некоторое значение перед их использованием, поэтому вам нужно вызвать либо настраиваемый конструктор, либо конструктор по умолчанию (конструктор, который не принимает параметров, всегда доступен для структуры).
В значительной степени структуры, которые считаются типами значений, размещаются в стеке, в то время как объекты выделяются в куче, а ссылка на объект (указатель) выделяется в стеке.
Хорошо, давай посмотрим, смогу ли я прояснить это.
Во-первых, Эш прав: вопрос в нет о том, где размещены значения типа переменные. Это другой вопрос, и ответ на него не просто «в стеке». Это сложнее (и еще более усложнено 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# может повторно использовать тот же стек слот. См. сообщение в блоге о построении типов значений Эрика Липперта для получения более подробной информации и случая, когда применяется не.
Я многому научился, написав этот ответ - пожалуйста, попросите разъяснений, если что-то из этого неясно!
Хороший ответ, Джон, прояснение сложности этой области очень важно. Я должен знать, что к настоящему времени очень редко можно получить черно-белый ответ на большинство вопросов, связанных с разработкой программного обеспечения.
Джон, пример кода HowManyStackAllocations хорош. Но не могли бы вы изменить его, чтобы использовать Struct вместо Guid, или добавить новый пример Struct. Я думаю, что тогда это напрямую затронет исходный вопрос @kedar.
Guid уже является структурой. См. msdn.microsoft.com/en-us/library/system.guid.aspx Я бы не выбрал ссылочный тип для этого вопроса :)
Последний бит этого ответа («но на самом деле стек выделяется только один раз, а затем то же место хранения используется повторно») оставил меня в замешательстве. Кажется, это противоречит этому: blogs.msdn.com/b/ericlippert/archive/2010/10/11/… Что мне не хватает?
@Ani: вам не хватает того факта, что в примере Эрика есть блок try / catch, поэтому, если во время конструктора структуры возникает исключение, вы должны иметь возможность видеть значение перед конструктором. В моем примере не такая ситуация - если конструктор не работает с исключением, не имеет значения, было ли значение guid перезаписано только наполовину, так как оно все равно не будет видно.
@Ani: Фактически, Эрик называет это в конце своего сообщения: «А как насчет точки зрения Веснера? Да, на самом деле, если объявлена локальная переменная, выделенная стеком (а не поле в замыкании) на том же уровне вложенности "попробуйте", что и вызов конструктора, тогда мы не будем выполнять эту ритуальную процедуру создания нового временного, инициализации временного и копирования его в локальный. В этом конкретном (и распространенном) случае мы можем оптимизировать создание временного и копии, потому что программа на C# не может заметить разницу! "
Конструктор без параметров для структуры присутствовал в более ранних предварительных версиях C# 6.0. Но потом его убрали. github.com/dotnet/roslyn/issues/1029
@JonSkeet Распределение кучи в ParameterisedCtorCallMethod меня удивляет. Я ожидал, что компилятор представит локальную переменную и вызовет ctor, принимающий строку в качестве параметра (как в ParameterisedCtorAssignToLocal). Вы знаете, почему он выбирает размещение в куче?
@Vagaus: Что заставляет вас думать, что он выделяется в куче? Несмотря на то, что он называется newobj, я не верю, что на самом деле он выполняет какое-либо распределение кучи.
@JonSkeet Я предполагал, что newobj всегда будет размещаться в куче, я думаю, мне нужно проверить спецификации :)
@JonSkeet отличный ответ, но правильно ли я понимаю, что в ParameterisedCtorAssignToField мы сначала выделяем значение в стеке, а затем копируем значение в кучу (а именно в поле field) с помощью инструкции stsfld?
@ Е.Щербо: Да, правильно.
Короче говоря, new - неправильное название структур, вызов new просто вызывает конструктор. Единственное место хранения для структуры - это местоположение, которое она определена.
Если это переменная-член, она сохраняется непосредственно в том, в чем она определена, если это локальная переменная или параметр, она хранится в стеке.
Сравните это с классами, у которых есть ссылка везде, где структура была бы сохранена целиком, в то время как ссылки указывают где-то в куче. (Член внутри, локальный / параметр в стеке)
Это может помочь немного взглянуть на C++, где нет реального различия между классом / структурой. (В языке есть похожие имена, но они относятся только к доступности вещей по умолчанию). Когда вы вызываете new, вы получаете указатель на расположение кучи, а если у вас есть ссылка без указателя, она сохраняется непосредственно в стеке или внутри другого объекта структуры ala на C#.
Я, наверное, что-то здесь упускаю, но почему нас волнует распределение?
Типы значений передаются по значению;) и поэтому не могут быть изменены в области, отличной от той, в которой они определены. Чтобы иметь возможность изменять значение, вам нужно добавить ключевое слово [ref].
Ссылочные типы передаются по ссылке и могут быть изменены.
Самыми популярными, конечно, являются строки неизменяемых ссылочных типов.
Макет / инициализация массива: Типы значений -> нулевая память [имя, почтовый индекс] [имя, почтовый индекс] Типы ссылок -> нулевая память -> null [ref] [ref]
Типы ссылок не передаются по ссылке - ссылки передаются по значению. Это совсем другое.
> references are passed by value ..., это ссылка. (Но это не то же самое, что передача ref-типов по ссылке)
Объявление class или struct похоже на схему, которая используется для создания экземпляров или объектов во время выполнения. Если вы определяете class или struct с именем Person, Person - это имя типа. Если вы объявляете и инициализируете переменную p типа Person, p считается объектом или экземпляром Person. Можно создать несколько экземпляров одного и того же типа Person, и каждый экземпляр может иметь разные значения в properties и fields.
class - эталонный тип. Когда объект class создается, переменная, которой назначен объект, содержит только ссылку на эту память. Когда ссылка на объект присваивается новой переменной, новая переменная ссылается на исходный объект. Изменения, внесенные в одну переменную, отражаются в другой переменной, потому что они оба относятся к одним и тем же данным.
struct - это тип значения. Когда создается struct, переменная, которой назначен struct, содержит фактические данные структуры. Когда struct назначается новой переменной, она копируется. Таким образом, новая переменная и исходная переменная содержат две отдельные копии одних и тех же данных. Изменения, внесенные в одну копию, не влияют на другую копию.
Как правило, classes используется для моделирования более сложного поведения или данных, которые должны быть изменены после создания объекта class. Structs лучше всего подходит для небольших структур данных, содержащих в основном данные, которые не предназначены для изменения после создания struct.
"структуры всегда идут туда, где они были объявлены", это немного сбивает с толку. Поле структуры в классе всегда помещается в «динамическую память, когда создается экземпляр типа» - Джефф Рихтер. Это может быть косвенно в куче, но это совсем не то же самое, что и обычный ссылочный тип.