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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
296
0
82 800
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

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

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

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

"структуры всегда идут туда, где они были объявлены", это немного сбивает с толку. Поле структуры в классе всегда помещается в «динамическую память, когда создается экземпляр типа» - Джефф Рихтер. Это может быть косвенно в куче, но это совсем не то же самое, что и обычный ссылочный тип.

Ash 15.10.2008 09:29

Нет, я думаю, что это точно правильно - хотя это не то же самое, что ссылочный тип. Значение переменной живет там, где оно объявлено. Значение переменной ссылочного типа - это ссылка, а не фактические данные, вот и все.

Jon Skeet 15.10.2008 09:32

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

Ash 15.10.2008 09:33

@Ash (недавний): ложь; если переменная "захвачена" (в лямбда / анон-метод), тогда это фактически поле в классе, созданном компилятором, то есть в куче

Marc Gravell 15.10.2008 09:37

Джон, ты упустил мою точку зрения. Причина, по которой этот вопрос был задан впервые, заключается в том, что многим разработчикам (включая меня, пока я не прочитал CLR Via C#) непонятно, где выделяется структура, если вы используете оператор new для ее создания. Сказать, что «структуры всегда идут туда, где они были объявлены» не является однозначным ответом.

Ash 15.10.2008 09:41

@Ash - также «блоки итератора» - для разработчика они выглядят как методы, но опять же, все переменные становятся полями, так что куча.

Marc Gravell 15.10.2008 09:44

@Marc, технически ты конечно прав, почему мне вспоминается этот анекдот? jokes2go.com/jokes/6342.html

Ash 15.10.2008 10:01

Эш: Да, вы должны понимать, что новый оператор в C# отличается от C++. Нет, это не значит, что правильно утверждать, что он всегда основан на стеке.

Jon Skeet 15.10.2008 10:07

Лучше, чем дать неправильный, но полезный ответ по-видимому. На самом деле, проблема стека и кучи связана с множеством последствий с захваченными переменными и блоками итератора - то есть без них он не работал бы. Итак, если вы считаете, что эти полезности ...

Marc Gravell 15.10.2008 10:07

Да, и что касается вашего «что неясно для разработчиков»: я видел, как многие люди были сбиты с толку утверждением, что типы значений всегда выделяются в стеке. Мне приходилось исправлять этот миф огромное количество раз в группах новостей.

Jon Skeet 15.10.2008 10:08

Эш: Для ясности, я понимаю, почему вы проводите различие между «где живут структуры» и «что делает новый (...)» - но я думаю, что вопрос требует тщательного ответа очень, чтобы избежать путаницы. Есть много укромных мест (например, вызов конструктора без параметров ...)

Jon Skeet 15.10.2008 10:13

@Jon, Справедливо, это хорошее обсуждение, которое помогло мне, и я уверен, что поможет и другим.

Ash 15.10.2008 10:25

@Ash: Если у меня будет время, я постараюсь написать ответ, когда доберусь до работы. Впрочем, это слишком большая тема, чтобы пытаться осветить ее в поезде :)

Jon Skeet 15.10.2008 10:34

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

Структуры

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.

Это не распространяется на случай, когда структура является частью класса - в этот момент она находится в куче вместе с остальными данными объекта.

Jon Skeet 15.10.2008 09:19

Да, но на самом деле он фокусируется на задаваемом вопросе и отвечает на него. Проголосовал.

Ash 15.10.2008 09:54

... при этом все еще неверный и вводящий в заблуждение. Извините, но на этот вопрос нет коротких ответов - Джеффри - единственный полный ответ.

Marc Gravell 15.10.2008 10:04

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

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

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

Хороший ответ, Джон, прояснение сложности этой области очень важно. Я должен знать, что к настоящему времени очень редко можно получить черно-белый ответ на большинство вопросов, связанных с разработкой программного обеспечения.

Ash 16.10.2008 04:36

Джон, пример кода HowManyStackAllocations хорош. Но не могли бы вы изменить его, чтобы использовать Struct вместо Guid, или добавить новый пример Struct. Я думаю, что тогда это напрямую затронет исходный вопрос @kedar.

Ash 18.10.2008 12:06

Guid уже является структурой. См. msdn.microsoft.com/en-us/library/system.guid.aspx Я бы не выбрал ссылочный тип для этого вопроса :)

Jon Skeet 18.10.2008 13:04

Последний бит этого ответа («но на самом деле стек выделяется только один раз, а затем то же место хранения используется повторно») оставил меня в замешательстве. Кажется, это противоречит этому: blogs.msdn.com/b/ericlippert/archive/2010/10/11/… Что мне не хватает?

Ani 17.12.2010 11:47

@Ani: вам не хватает того факта, что в примере Эрика есть блок try / catch, поэтому, если во время конструктора структуры возникает исключение, вы должны иметь возможность видеть значение перед конструктором. В моем примере не такая ситуация - если конструктор не работает с исключением, не имеет значения, было ли значение guid перезаписано только наполовину, так как оно все равно не будет видно.

Jon Skeet 17.12.2010 12:06

@Ani: Фактически, Эрик называет это в конце своего сообщения: «А как насчет точки зрения Веснера? Да, на самом деле, если объявлена ​​локальная переменная, выделенная стеком (а не поле в замыкании) на том же уровне вложенности "попробуйте", что и вызов конструктора, тогда мы не будем выполнять эту ритуальную процедуру создания нового временного, инициализации временного и копирования его в локальный. В этом конкретном (и распространенном) случае мы можем оптимизировать создание временного и копии, потому что программа на C# не может заметить разницу! "

Jon Skeet 17.12.2010 12:06

Конструктор без параметров для структуры присутствовал в более ранних предварительных версиях C# 6.0. Но потом его убрали. github.com/dotnet/roslyn/issues/1029

Eldar 18.02.2016 22:04

@JonSkeet Распределение кучи в ParameterisedCtorCallMethod меня удивляет. Я ожидал, что компилятор представит локальную переменную и вызовет ctor, принимающий строку в качестве параметра (как в ParameterisedCtorAssignToLocal). Вы знаете, почему он выбирает размещение в куче?

Vagaus 04.03.2020 18:43

@Vagaus: Что заставляет вас думать, что он выделяется в куче? Несмотря на то, что он называется newobj, я не верю, что на самом деле он выполняет какое-либо распределение кучи.

Jon Skeet 04.03.2020 19:29

@JonSkeet Я предполагал, что newobj всегда будет размещаться в куче, я думаю, мне нужно проверить спецификации :)

Vagaus 04.03.2020 19:35

@JonSkeet отличный ответ, но правильно ли я понимаю, что в ParameterisedCtorAssignToField мы сначала выделяем значение в стеке, а затем копируем значение в кучу (а именно в поле field) с помощью инструкции stsfld?

E. Shcherbo 10.12.2020 09:33

@ Е.Щербо: Да, правильно.

Jon Skeet 10.12.2020 09:54

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

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

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

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

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

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

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

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

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

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

Jon Skeet 03.06.2015 19:24

> references are passed by value ..., это ссылка. (Но это не то же самое, что передача ref-типов по ссылке)

user1234567 10.07.2020 15:05

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

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

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

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

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

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