Типы значений и ссылочные типы

Тип элемента данных определяет объем памяти (количество бит), необходимый для его хранения, а также набор членов данных, содержащихся в данном типе. Кроме того, тип определяет, где в памяти будет размещен объект: в стеке (Stack) или в куче (Heap).

Типы делятся на две основные группы: типы значений (Value Types) и ссылочные типы (Reference Types). Способ хранения объектов этих двух групп в памяти полностью различается.

1. Тип значения занимает один непрерывный участок памяти, в котором напрямую хранятся реальные данные.

Данные типов значений хранятся напрямую в стеке Stack, блок Data содержит само значение. Например, при записи int age = 12; число 12 сохраняется прямо в этой области стека

2. Ссылочный тип требует двух отдельных участков памяти:

  • Первый участок хранит реальные данные объекта, он всегда выделяется в куче;
  • Второй участок — это адрес ссылки, указывающий на расположение данных в куче. (В стеке хранится адрес из кучи, в эпоху языка C это называлось указателем)
Основные данные ссылочного типа хранятся в куче, им присваивается адрес вида 000001, а в стеке сохраняется этот адрес как ссылка Reference

Две приведенные иллюстрации корректны только для самостоятельных объектов, не являющихся членами данных другого объекта.

Важное замечание

Основная часть данных любого объекта ссылочного типа всегда размещается в куче, как показано на рисунках выше.

Однако экземпляры типов значений или адреса ссылок ссылочных типов могут находиться как в стеке, так и в куче — это зависит от структуры написанного кода.

Пример:

Предположим, существует ссылочный тип Person, содержащий два члена: один член типа значения и один ссылочный член.

Многие разработчики ошибочно полагают, что члены типов значений хранятся в стеке, адреса ссылок тоже в стеке, а основные данные объекта в куче (как на рисунках выше), но это неверное утверждение.

Запомните одно правило: все основные данные экземпляра ссылочного типа навсегда выделяются в куче. Оба члена относятся к основным данным этого объекта, поэтому независимо от того, являются ли они типами значений или ссылочными типами, они хранятся вместе в куче.

Пример кода:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }
}   

class Phone
{
    public long number;
}Code language: JavaScript (javascript)

Рассмотрим пример выше: класс Person содержит стандартные поля age и name, а также пользовательский тип Phone. Тип Phone, в свою очередь, содержит стандартный тип long для хранения номера телефона.

Читатель может увидеть int age внутри Person или long number внутри Phone и решить, что эти типы значений хранятся в стеке, но это не так. Несмотря на то что это стандартные встроенные типы, они являются членами пользовательского класса, поэтому хранятся в куче вместе с экземпляром объекта. Итоговое расположение в памяти выглядит так:

На рисунке видно, что int age и long number являются встроенными типами значений, но как члены пользовательского типа они хранятся в куче. Поле myPhone содержит адрес ссылки, значит, даже ссылки ссылочных типов могут располагаться в куче.

Основное правило: все члены данных пользовательских классов хранятся в куче, будь то типы значений или ссылочные типы. Если внутри класса есть ссылочный член, как myPhone в примере, то в этом поле записан адрес другого объекта, расположенного в куче.

Если внутри класса Phone будет объявлен еще один пользовательский тип, то в области памяти Phone появится дополнительный участок для хранения адреса ссылки на этот второй объект.

Выполнение программы начинается с функции Main. Адрес самого верхнего ссылочного объекта хранится в стеке, а далее все связанные объекты выстраиваются цепочкой в куче.

Ссылки образуют связанную цепочку

Другой пример

// Ссылочный тип — класс
class MyType
{
    public int A;       // Член типа значения, встроен в экземпляр MyType в куче
    public Person B;    // Ссылочный член; указатель ссылки хранится в куче, основные данные выделены в отдельном участке кучи
}

class Person
{
    public string Name;
}

void Test()
{
    // В стеке хранится указатель ссылки на myObj
    MyType myObj = new MyType(); 
    /*
    Структура кучи:
    Объект MyType {
        A: значение int, сохраненное встроенно
        B: указатель ссылки на экземпляр Person
    }
    Отдельный участок кучи: Person{ Name: ... }
    */

    // Локальная переменная типа значения, выделена напрямую в стеке
    int localNum = 10;
}Code language: PHP (php)

Описанные выше правила относятся исключительно к членам данных класса, а не к его методам. Если в методе класса объявлены локальные переменные или ссылочные объекты, то локальные типы значений и адреса ссылочных экземпляров по-прежнему хранятся в стеке. Смотрите код ниже:

Рассмотрим следующий пример:

using System.ComponentModel.DataAnnotations;

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
        bill.Calculate();
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }

    public int Calculate()
    {
        int one = 12;
        int two = 23;
        Phone myNewPhone = new Phone();
        return one + two;
    }
}   

class Phone
{
    public long number;
}Code language: PHP (php)

В этом коде я добавил метод Calculate в класс Person. Внутри метода объявлены локальные переменные как типов значений, так и ссылочных типов, вызываемые из Main. Эти переменные не являются членами класса Person, они относятся к локальной области видимости метода: локальные типы значений хранятся в стеке, адреса ссылок локальных ссылочных объектов тоже сохраняются в стеке.

При выполнении строки bill.Calculate(); расположение данных в памяти выглядит так:

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

Как только завершится выполнение метода, все эти локальные переменные выгружаются из стека, структура памяти принимает следующий вид:

После завершения работы Calculate управление возвращается в Main, все локальные переменные, созданные внутри метода, удаляются из стека. Однако основные данные объекта myNewPhone (синяя часть на рисунке) остаются в куче. Из стека удаляется только адрес ссылки, реальные данные не уничтожаются сразу — они остаются в куче до момента сборки мусора GC.

Типы значений и ссылочные типы

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *