Перейти к содержимому
Теория · Значимые и ссылочные типы · урок 1 из 11

Два вида типов

Чему вы научитесь

Чему вы научитесь:

  • отличать значимые типы от ссылочных и предсказывать, что копирует присваивание;
  • объявлять структуры и передавать в методы структуры и объекты, с ref и без;
  • объяснять упаковку и работать с null через ?., ??, int? и string?;
  • сравнивать объекты и структуры через == и Equals.
У int два отдельных листа, и исчёркана только копия, а у Counter две ссылки ведут к одному исчёрканному документу

Коллеге нужен ваш отчёт, и отдать его можно двумя способами. Можно сделать ксерокопию: у коллеги будет свой лист, он может исчёркать его как угодно, а ваш останется прежним. А можно прислать ссылку на документ в облаке. Тогда вы оба открываете один и тот же документ, и правку одного сразу видит другой.

В C# присваивание и передача аргумента в метод тоже работают одним из этих двух способов. Оба вы уже встречали: числа при присваивании копировались, а массивы и объекты классов получали второе имя. Посмотрим на них рядом:

C#
int a = 1;
int b = a;
b = 2;
Console.WriteLine(a); // 1

Counter first = new Counter();
Counter second = first;
second.Value = 2;
Console.WriteLine(first.Value); // 2

class Counter
{
    public int Value;
}

Давайте разберём пример по строкам:

  1. int b = a; делает ксерокопию: в b попадает своё число 1.
  2. b = 2 меняет только b, поэтому a по-прежнему печатает 1.
  3. Counter second = first; копирует не объект, а ссылку на него. Это вторая ссылка на тот же документ.
  4. second.Value = 2 меняет общий объект, и first.Value тоже печатает 2.

Одно и то же присваивание = дало два разных результата.

Здесь аналогия неточна. В жизни ксерокопию делают, только когда о ней попросят. В C# выбора при присваивании нет: копируется ли значение или ссылка, решает тип, а не тот, кто пишет =.

Значимые типы

Переменная значимого типа хранит само значение. Присваивание и передача в метод копируют его целиком, и копии дальше живут отдельно. Вы уже встречали это поведение:

  • в теме о переменных: после b = a и a = 7 в b оставалось старое число;
  • в теме о методах: метод получал копию аргумента, а саму переменную — через ref.

Значимые типы — это все встроенные числовые типы (int, long, double, decimal), а также bool и char. Свой значимый тип тоже можно объявить, но словом struct, а не class.

Ссылочные типы

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

  • массивы: после int[] b = a; у одного массива два имени;
  • string: одну строку могут делить несколько переменных, и это безопасно, потому что строку никто не изменит;
  • все классы: переменная класса хранит ссылку на объект.

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

Стек и куча

Откуда берётся разница? Чтобы это понять, посмотрим, где программа хранит данные. Упрощённая картина такая:

  • стек — уже знакомый вам стек вызовов: каждый вызов получает в нём свою область для локальных переменных и освобождает её, когда метод завершается;
  • куча — общая память для объектов: объект, созданный через new, лежит там, пока на него ведёт хоть одна ссылка.

Когда ссылок на объект не остаётся, сборщик мусора — часть среды выполнения .NET — со временем освобождает его память.

Локальная переменная int хранит число прямо в стеке. Локальная переменная класса хранит в стеке только ссылку, а сам объект лежит в куче. Это и есть ссылка на облачный документ: документ один, а ссылок на него может быть сколько угодно.

В виджетах курса каждый вызов метода нарисован отдельной рамкой — это его область в стеке. Стрелка → у переменной ведёт к объекту в куче.

Где хранится значимый тип

Картинка со стеком и кучей упрощена: поле значимого типа лежит внутри своего объекта, то есть в куче, а в некоторых конструкциях языка компилятор переносит в кучу и локальные переменные.

На собеседовании часто спрашивают: «Значимые типы хранятся на стеке?» Точный ответ — значимый тип хранится там, где объявлена переменная:

  • локальная переменная метода обычно лежит в стеке;
  • поле объекта лежит вместе с объектом в куче;
  • элемент массива int[] лежит внутри массива, тоже в куче.

Значимый тип от ссылочного отличает то, что копирует присваивание, а не место в памяти.

Частая ошибка

Перед тем как сбросить счётчик, решили сохранить «резервную копию» через =:

C#
Counter current = new Counter();
current.Value = 5;
Counter backup = current; // ✗
current.Value = 0;
Console.WriteLine(backup.Value); // 0

Программа компилируется и печатает 0. Counter backup = current; не создаёт второй объект: backup — вторая ссылка на тот же самый. Сбросив current.Value, вы сбросили и «копию».

Чтобы сохранить старое значение, скопируйте само число: int saved = current.Value;. Тип int значимый, поэтому saved не зависит от объекта.

Главное

  • Переменная значимого типа хранит само значение, и присваивание копирует его целиком.
  • Переменная ссылочного типа хранит ссылку: присваивание копирует ссылку, и объект становится общим.
  • Значимые типы — числа, bool, char и типы struct; ссылочные — массивы, string и классы.
  • Стек и куча — упрощённая картина: значимый тип хранится там, где объявлена переменная.