This lesson is not translated yet – the Russian original is below.
Два типа хранят число 5 – и ведут себя по-разному при копировании. Разобраться, почему, стоит
один раз: отсюда растёт половина неожиданного поведения в C#, и вся история про null тоже
начинается здесь.
Две категории
Значимые типы (value types) хранят значение прямо в переменной. Это все числа, bool, char,
а также структуры и перечисления.
Ссылочные типы (reference types) хранят адрес: сам объект лежит отдельно, а в переменной –
только указание, где его искать. Это классы, массивы, string и все интерфейсы.
Почему это заметно
Разница вылезает при копировании. Значимый тип копируется целиком:
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 ?? "не указан";