Course program← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Значимые и ссылочные типы, null

This lesson is not translated yet – the Russian original is below.

Два типа хранят число 5 – и ведут себя по-разному при копировании. Разобраться, почему, стоит один раз: отсюда растёт половина неожиданного поведения в C#, и вся история про null тоже начинается здесь.

Две категории

Значимые типы (value types) хранят значение прямо в переменной. Это все числа, bool, char, а также структуры и перечисления.

Ссылочные типы (reference types) хранят адрес: сам объект лежит отдельно, а в переменной – только указание, где его искать. Это классы, массивы, string и все интерфейсы.

значимый тип ссылочный тип int a Car myCar объект Car 5 адрес Color, Model значение внутри переменной объект лежит отдельно

Почему это заметно

Разница вылезает при копировании. Значимый тип копируется целиком:

int a = 5;
int b = a;      // скопировали значение
b = 10;

Console.WriteLine(a);    // 5 – не изменилось

Ссылочный тип копирует только адрес, а объект остаётся один на двоих:

int[] first = { 1, 2, 3 };
int[] second = first;    // скопировали ссылку, не массив
second[0] = 99;

Console.WriteLine(first[0]);    // 99 – это тот же самый массив

Второй пример удивляет тех, кто ждал копию. Копии не было: first и second – два имени одного объекта.

Чтобы получить настоящую копию массива, её нужно попросить явно: int[] copy = (int[])first.Clone(); или first.ToArray(). Присваивание копий не делает никогда.

Как с этим в других языках

Одна и та же операция – присвоили и изменили:

int[] second = first;
second[0] = 99;         // first тоже изменился: одна ссылка на двоих
second = first
second[0] = 99          # то же самое: списки в Python всегда по ссылке
second := first         // массив в Go – значимый тип, это настоящая копия
second[0] = 99          // first не изменился

C# и Python ведут себя одинаково, потому что оба различают значимые и ссылочные типы примерно одинаково. Go делит иначе: массив там копируется целиком, а по ссылке передаются срезы. Одна и та же строчка кода означает разное – поэтому «работает как в том языке» плохой ориентир.

Строка притворяется значимым типом

string – ссылочный тип, но ведёт себя как значимый, потому что он неизменяемый. Изменить существующую строку невозможно: любая операция создаёт новую.

string a = "привет";
string b = a;
b = b.ToUpper();          // создалась новая строка, a осталась прежней

Console.WriteLine(a);     // привет

Из-за неизменяемости строку невозможно испортить из чужого кода – и потому её можно спокойно передавать куда угодно. Подробнее об этом в модуле про строки, там же будет про цену такого поведения.

null: ссылка в никуда

Ссылочная переменная может не указывать никуда. Это состояние называется null:

string name = null;

Console.WriteLine(name.Length);    // NullReferenceException

У null нет длины, свойств и методов – обращаться не к чему. Программа падает с NullReferenceException, самой частой ошибкой времени выполнения в C#.

Значимый тип быть null не может: в переменной int всегда лежит число, пусть даже ноль. Ноль – это «ноль», а не «ничего», и путать их не надо.

Значимый тип, который всё-таки может быть пустым

Иногда «значения нет» – осмысленное состояние: оценка ещё не выставлена, дата не указана. Для этого к типу дописывают вопросительный знак:

int? score = null;       // «оценки нет»

if (score.HasValue)
{
    Console.WriteLine(score.Value);
}

Короткая форма получения значения с запасным вариантом – оператор ??:

int actual = score ?? 0;    // если null, то 0

Проверки компилятора

В новых проектах включён <Nullable>enable</Nullable>, и он меняет правила для ссылочных типов:

string name = null;      // предупреждение: тип не допускает null
string? maybe = null;    // а так можно – вы объявили это намеренно

Компилятор начинает следить за тем, где значение может отсутствовать, и предупреждает при обращении без проверки. Это не запрет – программа соберётся. Но предупреждения здесь стоит читать: каждое из них показывает место будущего NullReferenceException.

Именно поэтому Nullable в шаблоне проекта включён по умолчанию. Выключить его можно, и в старых проектах он выключен, но тогда вся защита от null остаётся на внимательности программиста.

Два оператора, которые экономят проверки

Условный доступ ?. – обратиться к члену, только если слева не null:

int? length = name?.Length;    // null, если name равно null

Всё выражение становится null, а падения не происходит. Вызовы можно выстраивать в цепочку: order?.Customer?.Address?.City не упадёт, даже если пусто окажется первое же звено.

Оператор ?? – подставить запасное значение:

string display = name ?? "аноним";

Вместе они дают компактную и безопасную запись, которая заменяет три вложенных if:

string city = order?.Customer?.Address?.City ?? "не указан";