Коли ви створюєте екземпляр класу з new
оператором, пам'ять виділяється на купі. Коли ви створюєте екземпляр структури з new
оператором, куди виділяється пам'ять, на купі чи на стеку?
Коли ви створюєте екземпляр класу з new
оператором, пам'ять виділяється на купі. Коли ви створюєте екземпляр структури з new
оператором, куди виділяється пам'ять, на купі чи на стеку?
Відповіді:
Гаразд, давайте подивимось, чи можу я зробити це ясніше.
По-перше, Еш правий: питання не в тому, де розподіляються змінні типу значень . Це вже інше питання - і те, на яке відповідь не просто "на стеці". Це складніше, ніж це (і ще більше ускладнюється C # 2). У мене є стаття на цю тему, і я розширюю її, якщо вимагатиме, але давайте розберемося лише з new
оператором.
По-друге, все це насправді залежить від того, про який рівень ви говорите. Я дивлюся, що компілятор робить із вихідним кодом, з точки зору IL, який він створює. Більш ніж можливо, що компілятор JIT зробить розумні речі з точки зору оптимізації досить "логічного" виділення.
По-третє, я ігнорую дженерики, здебільшого тому, що насправді не знаю відповіді, а частково тому, що це занадто ускладнить речі.
Нарешті, все це якраз із поточною реалізацією. Специфікація C # не вказує багато цього - це фактично деталі реалізації. Є такі, хто вважає, що розробникам керованого коду насправді не варто хвилювати. Я не впевнений, що хотів би так далеко, але варто уявити собі світ, де насправді всі локальні змінні живуть на купі - який би все-таки відповідав специфікації.
Існує дві різні ситуації з new
оператором щодо типів значень: ви можете викликати конструктор без параметрів (наприклад new Guid()
) або конструктор параметрів (наприклад new Guid(someString)
). Вони генерують значно різні ІЛ. Щоб зрозуміти чому, вам потрібно порівняти специфікації C # і CLI: згідно з C #, всі типи значень мають конструктор без параметрів. Відповідно до специфікації CLI, жодні типи значень не мають конструкторів без параметрів. (Виконайте конструктор типу значення з відображенням деякий час - ви не знайдете його без параметрів.)
Для C # має сенс трактувати "ініціалізувати значення з нулями" як конструктор, тому що він підтримує мову послідовною - можна подумати, new(...)
як завжди викликати конструктор. CLI має сенс думати про це по-різному, оскільки немає справжнього коду для виклику - і, звичайно, немає коду, характерного для типу.
Це також має значення, що ви збираєтеся робити зі значенням після його ініціалізації. IL використовували для
Guid localVariable = new Guid(someString);
відрізняється від ІР, що використовується для:
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
виділяє простір на стеку - але, як ми бачили, це не те, що відбувається насправді навіть на рівні ІЛ. Я хотів би виділити один конкретний випадок. Візьміть цей метод:
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
конструктор викине виняток, саме тому компілятор C # може повторно використовувати той самий слот стека. Докладнішу інформацію див. У блозі Еріка Ліпперта про створення типу вартості для отримання детальної інформації та випадку, коли воно не застосовується.
Я багато чого навчився писати цю відповідь - будь ласка, попросіть роз'яснення, якщо щось із них незрозуміле!
List<Guid>
і додайте до них ці 3? Це були б 3 виділення (та сама ІЛ)? Але вони зберігаються десь магічними
guid
було лише перезаписано, оскільки воно все одно не буде видно.
Пам'ять, що містить поля структури, може бути розподілена як у стеці, так і в купі залежно від обставин. Якщо змінна type-типу - це локальна змінна або параметр, який не захоплюється деяким анонімним делегатом або класом ітератора, то він буде виділений у стеці. Якщо змінна є частиною якогось класу, то вона буде розподілена всередині класу на купі.
Якщо структура розподілена на купі, то для виклику нового оператора фактично не потрібно виділяти пам'ять. Єдиною метою було б встановити значення поля відповідно до того, що є в конструкторі. Якщо конструктор не викликається, то всі поля отримають свої значення за замовчуванням (0 або null).
Аналогічно і для структур, виділених у стеку, за винятком того, що C # вимагає, щоб усі локальні змінні були встановлені на якесь значення перед їх використанням, тому вам доведеться викликати або власний конструктор, або конструктор за замовчуванням (конструктор, який не приймає жодних параметрів, завжди доступний для конструкції).
Простіше кажучи, нове - це неправильне значення для структур, виклик нового просто викликає конструктор. Єдине місце зберігання структури - це визначене місце.
Якщо це змінна-член, вона зберігається безпосередньо в тому, що вона визначена, якщо вона є локальною змінною або параметром, вона зберігається в стеці.
Протиставляйте це класам, які мають посилання, де структура зберігалася б у повному обсязі, тоді як опорні точки десь на купі. (Учасник, локальний / параметр у стеці)
Це може допомогти трохи заглянути в C ++, де немає різниці між класом / структурою. (У мові є подібні імена, але вони стосуються лише доступності речей за замовчуванням) Коли ви зателефонуєте до нового, ви отримаєте вказівник на місце купи, тоді як, якщо у вас є посилання, що не вказує, воно зберігається безпосередньо у стеку або в межах іншого об'єкта, ala будує в C #.
Як і для всіх типів цінності, структури завжди йдуть туди, де вони були оголошені .
Дивіться це питання тут, щоб отримати докладнішу інформацію про те, коли використовувати структури. І це питання тут для отримання додаткової інформації про структури.
Редагувати: я помилково відповів, що ЗАВЖДИ вони йдуть у групі. Це неправильно .
Я, мабуть, щось тут пропускаю, але чому ми дбаємо про виділення?
Типи значень передаються за значенням;) і, таким чином, не можуть бути мутовані в іншому масштабі, ніж там, де вони визначені. Щоб мати змогу вимкнути значення, потрібно додати ключове слово [ref].
Типи посилань передаються шляхом посилання та можуть бути вимкнено.
Звичайно, найпопулярнішими є рядки незмінних посилальних типів.
Макет / ініціалізація масиву: типи значень -> нульова пам'ять [ім'я, zip] [ім'я, zip] Типи довідок -> нульова пам'ять -> null [ref] [ref]
class
Або struct
декларація , як план , який використовується для створення екземплярів або об'єктів під час виконання. Якщо ви визначаєте особу class
або struct
називаєте її особою, особа - це назва типу. Якщо ви оголошуєте та ініціалізуєте змінну p типу Person, p, як кажуть, є об'єктом або екземпляром Person. Можна створити кілька примірників одного типу Person, і кожен екземпляр може мати різні значення у своїх properties
та fields
.
А class
- еталонний тип. Коли створюється об'єкт class
, змінна, якій призначений об'єкт, містить лише посилання на цю пам'ять. Коли посилання на об'єкт присвоюється новій змінній, нова змінна відноситься до вихідного об'єкта. Зміни, внесені через одну змінну, відображаються в іншій змінній, оскільки вони обоє посилаються на одні і ті ж дані.
A struct
- тип значення. Коли а struct
створюється, змінна, якій struct
присвоєно, містить фактичні дані структури. Коли struct
присвоєно новій змінній, вона копіюється. Нова змінна та оригінальна змінна тому містять дві окремі копії одних і тих же даних. Зміни, внесені до однієї копії, не впливають на іншу.
Взагалі, classes
використовуються для моделювання більш складної поведінки або даних, які призначені для зміни після створення class
об'єкта. Structs
найкраще підходять для невеликих структур даних, які містять в основному дані, які не призначені для зміни після struct
створення.
В основному структури, що вважаються типом значення, виділяються на стек, в той час як об'єкти виділяються на купі, тоді як посилання на об'єкт (вказівник) виділяється на стек.
Конструкції виділяються в стек. Ось корисне пояснення:
Крім того, класи, коли інстанціюється в .NET, виділяють пам’ять на купі або в резервованому просторі пам'яті .NET. Тоді як конструкції приносять більшу ефективність при інстанціюванні завдяки розподілу на стеку. Крім того, слід зазначити, що передача параметрів у структурах робиться це за значенням.