Как происходит инициализация переменных в C# и где они "лежат" перед созданием?
Есть несколько переменных:
var a = 1;
var b = "string";
var c = false;
var d = new MyClass();
Можете объяснить, где лежат значения этих переменных перед их инициализацией?
Правильно ли я понимаю, что на этапе компиляции значения помещаются в кучу, а в рантайме из кучи берутся значения?
Ответы (1 шт):
Глобально память можно поделить на 2 области: стек и куча.
Размер стека всегда известен и фиксирован ещё на этапе компиляции внутри конкретного блока кода. В нём хранятся переменные локальной области видимости
В куче же хранятся данные, размер которых (на этапе компиляции) не известен.
Я немного всё упрощу для понимания, но основные концепции понять можно
Давайте не основе примера разберём, как именно выделяется память. Допустим у нас есть блок кода:
{
int x;
string str = "Hello world!";
List<int> intList = new() {1, 2, 3};
MyClass anyClass = new();
MyStruct anyStruct = new();
}
Представим стек как массив байт: [...]. Когда мы открываем новый блок кода мы грубо говоря объявляем, что сейчас в стеке будет использовано ещё N байтов. Для этого нам заранее (ещё на этапе компиляции) должен быть известен размер данного блока кода.
Однако не все типы фиксированного размера. Типы, размер которых неизвестен на этапе компиляции лежат в куче. Таким образом в C# появляется разделение на ссылочные типы и типы значений
Размер типов значений нам известен на этапе компиляции. Для int это например 4 байта.
Ссылочные же типы превращаются в значение адреса в памяти самого объекта, т. е. в nint (тип, размер которого зависит от разрядности системы). Сам объект же лежит в куче, и при обращении к нему мы его достаём из него.
P. S.: Для упрощения представим, что тип строки представляет из себя ссылочный тип, так как там свои тонкости.
В связи с этой информацией перепишем код выше на идентичный, но более близкий к компилятору:
{
int x;
nint strPointer;
nint listPoiner;
nint classPointer;
MyStruct anyStruct; // Структуры являются типом значений
}
Теперь мы можем вычислить размер данного блока кода сложив размеры всех локальных полей (для этого можно использовать встроенную функцию sizeof(T), но для ссылочных типов оно возвращает размер самого объекта, а не его ссылки):
int32 (4 байта) +
int64(8 байтов) +
int64(8 байтов) +
int64(8 байтов) +
MyStruct (сумма размеров всех полей, допустим 4 * 3 байта) =
40 байтов
Теперь на стеке в том же порядке мы выделяем эти 40 байтов под наши типы:
[..., int32_0, int32_1, int32_2, int32_3, int64_0, int64_1, ...]
Что бы получить доступ к переменной x нам надо получить sizeof(int) (4) байтов со сдвигом 0 относительно текущего блока кода.
Что бы получить доступ к ссылке на строку нам надо получить sizeof(int64) (8) байтов со сдвигом суммы всех предыдущих размеров (sizeof(int) байтов, так как перед ним стоит только int)
Что бы получить доступ к данным anyStruct переменной, нам надо получить sizeof(MyStruct) (12) байтов со сдвигом sizeof(int) + sizeof(int64) + sizeof(int64) + sizeof(int64) (28) байтов
Т. е. грубо говоря для данного блока кода относительно его начала внутри стека стек всегда будет выглядеть так:
[..., int, int64, int64, int64, MyStruct, ...]
Отвечая на главный вопрос:
Перед инициализацией они лежат в стеке, и C# по умолчанию присваивает им значения по умолчанию.
Если же говорить про объекты, то до инициализации в куче они могут вообще не существовать (null значит нулевая ссылка, т. е. на ничто)