Многие люди вводят в заблуждение, объясняя разницу между типами значений и ссылочными типами тем, что «типы значений помещаются в стек, а ссылочные типы - в кучу». Это просто неправда (как указано), и эта статья пытается несколько прояснить ситуацию.
Что в переменной?
Ключом к пониманию того, как работает память в .NET, является понимание того, что такое переменная и каково ее значение. На самом базовом уровне переменная - это просто связь между именем (используемым в исходном коде программы) и слотом памяти. У переменной есть значение, которое представляет собой содержимое слота памяти, с которым она связана. Размер этого слота и интерпретация значения зависят от типа переменной - и именно здесь проявляется разница между типами значений и ссылочными типами.
Значение переменной ссылочного типа всегда либо ссылка, либо null. Если это ссылка, это должна быть ссылка на объект, совместимый с типом переменной. Например, переменная, объявленная как Stream sвсегда, будет иметь значение, которое является либо nullссылкой на экземпляр Streamкласса, либо ссылкой на него . (Обратите внимание, что экземпляр подкласса Stream, например FileStream, также является экземпляром Stream.) Слот памяти, связанный с переменной, - это просто размер ссылки, каким бы большим ни был фактический объект, на который она ссылается. (Например, в 32-разрядной версии .NET слот переменной ссылочного типа всегда составляет всего 4 байта.)
Значение типа значения - это всегда данные для экземпляра самого типа. Например, предположим, что у нас есть структура, объявленная как:
struct PairOfInts { public int a; public int b; } |
Значение переменной, объявленной как PairOfInts pairсама пара целых чисел, а не ссылка на пару целых чисел. Слот памяти достаточно велик, чтобы вместить оба целых числа (поэтому он должен быть 8 байтов). Обратите внимание, что переменная типа значения никогда не может иметь значение null- это не имело бы никакого смысла, как nullи концепция ссылочного типа, означающая, что «значение этой переменной ссылочного типа вообще не является ссылкой на какой-либо объект».
Так где же хранятся вещи?
Слот памяти для переменной хранится либо в стеке, либо в куче. Это зависит от контекста, в котором он объявлен:
- Каждая локальная переменная (т. Е. Объявленная в методе) хранится в стеке. Это включает переменные ссылочного типа - сама переменная находится в стеке, но помните, что значение переменной ссылочного типа является только ссылкой (или
null), а не самим объектом. Параметры метода также считаются локальными переменными, но если они объявлены сrefмодификатором, они не получают своего собственного слота, но совместно используют слот с переменной, используемой в вызывающем коде. Подробнее см. Мою статью о передаче параметров . - Переменные экземпляра для ссылочного типа всегда находятся в куче. Вот где «живет» сам объект.
- Переменные экземпляра для типа значения хранятся в том же контексте, что и переменная, объявляющая тип значения. Слот памяти для экземпляра фактически содержит слоты для каждого поля в экземпляре. Это означает (с учетом двух предыдущих пунктов), что структурная переменная, объявленная внутри метода, всегда будет в стеке, тогда как структурная переменная, которая является полем экземпляра класса, будет находиться в куче.
- Каждая статическая переменная хранится в куче, независимо от того, объявлена ли она в ссылочном типе или типе значения. Всего существует только один слот, независимо от того, сколько экземпляров создано. (Тем не менее, не нужно создавать никаких экземпляров для существования этого единственного слота.) Подробная информация о том, в какой именно куче находятся переменные, сложна, но подробно объясняется в статье MSDN по этому вопросу .
Есть несколько исключений из приведенных выше правил - захваченные переменные (используемые в анонимных методах и лямбда-выражениях) являются локальными с точки зрения кода C #, но в конечном итоге компилируются в переменные экземпляра в типе, связанном с делегатом, созданным анонимным метод. То же самое касается локальных переменных в блоке итератора .
Наработанный пример
Все вышеперечисленное может показаться немного сложным, но полный пример должен прояснить ситуацию. Вот короткая программа, которая не делает ничего полезного, но должна продемонстрировать вышеизложенные моменты.
используя Систему; struct PairOfInts { статический счетчик int = 0; public int a; public int b; внутренний PairOfInts ( int x, int y) { а = х; b = y; счетчик ++; } } класс Test { PairOfInts pair; имя строки ; Тест (PairOfInts p, строка s, int x) { пара = р; name = s; пара. a + = x; } статическая пустота Main () { PairOfInts z = новый PairOfInts (1, 2); Тест t1 = новый тест (z, «первый» , 1); Тест t2 = новый тест (z, «второй» , 2); Тест t3 = null ; Тест t4 = t1; // XXX } } |
Посмотрим, что где в памяти на строчке, помеченной комментарием «XXX». (Предположим, что сборщик мусора ничего не делает.)
- В
PairOfIntsстеке есть экземпляр, соответствующий переменнойz. Внутри этого экземпляраa=1иb=2. (8-байтовый слот, необходимый дляzсебя, может быть представлен в памяти как01 00 00 00 02 00 00 00.) - В
Testстеке есть ссылка, соответствующая переменнойt1. Эта ссылка относится к экземпляру в куче, который занимает «что-то вроде» 20 байтов: 8 байтов информации заголовка (которая есть у всех объектов кучи), 8 байтов дляPairOfIntsэкземпляра и 4 байта для ссылки на строку. («Что-то вроде» возникает из-за того, что в спецификации не говорится, как это должно быть организовано, или какого размера заголовок и т. Д.) Значениеpairпеременной в этом экземпляре будет иметьa=2иb=2(возможно, представленное в памяти как02 00 00 00 02 00 00 00). Ценностьnameпеременная в этом экземпляре будет ссылкой на строковый объект (который также находится в куче) и который (возможно, через другие объекты, такие как массив символов) представляет последовательность символов в слове «первый». - В
Testстеке есть вторая ссылка, соответствующая переменнойt2. Эта ссылка относится ко второму экземпляру в куче, который очень похож на описанный выше, но со ссылкой на строку, представляющую «второй», а не «первый», и со значениемpairwherea=3(поскольку было добавлено 2 к исходному значению 1). ЕслиPairOfIntsбы тип был ссылочным, а не типом значения, то во всей программе был бы только один его экземпляр и всего несколько ссылок на единственный экземпляр, но на самом деле существует несколько экземпляров, каждый с разными значениями внутри. - В
Testстеке есть третья ссылка, соответствующая переменнойt3. Эта ссылкаnull- она не относится ни к одному из экземпляровTest. (Есть некоторая двусмысленность в отношении того, считается ли этоTestссылкой или нет - на самом деле это не имеет никакого значения - я обычно думаю,nullчто это ссылка, которая не ссылается на какой-либо объект, а не отсутствие ссылки Спецификация языка Java дает довольно приятную терминологию, говоря, что ссылка является либоnullуказателем, либо указателем на объект соответствующего типа.) - В
Testстеке есть четвертая ссылка, соответствующая переменнойt4. Эта ссылка относится к тому же экземпляру, что иt1- т. Е. Значенияt1иt4совпадают. Изменение значения одной из этих переменных не приведет к изменению значения другой, но изменение значения в объекте, на который они оба ссылаются с использованием одной ссылки, сделает это изменение видимым через другую ссылку. (Например, если вы установили, аt1.name="third";затем проверилиt4.name, вы обнаружите, что это тоже относится к «третьему».) - Наконец, есть
PairOfInts.counterпеременная, которая находится в куче (поскольку она статична). Для переменной есть только один «слот», сколь бы много (или мало)PairOfIntsзначений ни было.
Вернуться на главную страницу.
Комментариев нет:
Отправить комментарий