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

int? и string?

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

Положить в int значение null нельзя. Значимый тип хранит само значение, а не ссылку, и строка int age = null; не скомпилируется: компилятор ответит ошибкой CS0037. С вашей структурой то же самое: строка Point p = null; не скомпилируется.

Представьте анкету, где рядом с полем «Возраст» стоит галочка «заполнено». В C# для этого есть типы с вопросительным знаком: int? хранит число и отметку о том, есть ли оно.

Тип int?

Типом, допускающим null, называют значимый тип со знаком ?: int?, double?, bool?. Полное имя int? — Nullable<int>. Это структура из двух частей: само значение и флаг, есть ли оно.

C#
int? age = null;
Console.WriteLine(age.HasValue);  // False
Console.WriteLine(age ?? -1);     // -1
age = 30;
Console.WriteLine(age.HasValue);  // True
Console.WriteLine(age.Value + 1); // 31

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

  1. int? age = null; — поле пустое, галочки нет.
  2. age.HasValue печатает False: значения нет.
  3. age ?? -1 подставляет -1. Оператор ?? работает с int? так же, как со ссылками.
  4. После age = 30 значение есть, и HasValue печатает True.
  5. age.Value читает число, поэтому age.Value + 1 печатает 31.

Здесь анкета и int? расходятся. На бумаге можно вписать число и забыть поставить галочку. В int? галочка ставится сама: присвоили число — HasValue стал true.

int? и int — разные типы. Строка int x = age; не скомпилируется (ошибка CS0266): в age может не оказаться числа. Число достают явно: через age ?? 0 или через age.Value, когда вы проверили HasValue.

?. даёт int?

У строки свойство Length — типа int. Но если строки нет, то нет и длины, поэтому word?.Length даёт int?:

C#
string? word = null;
int? length = word?.Length;
Console.WriteLine(length.HasValue); // False
int safe = word?.Length ?? 0;
Console.WriteLine(safe); // 0

word?.Length при null в word даёт пустой int?, и HasValue печатает False. Чтобы получить обычный int, добавляют ?? 0: тогда вместо пустого значения подставится 0.

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

Ссылочная переменная может хранить null всегда: так устроены ссылки. Поэтому у ссылочного типа знак ? значит другое — это подсказка компилятору:

  • string — здесь null не ожидается;
  • string? — здесь может оказаться null.

В новых проектах и в программах, запущенных через dotnet run hello.cs, включена проверка, которую называют ссылочными типами, допускающими null. Компилятор по этим подсказкам следит, куда может попасть null, и предупреждает об опасных местах:

C#
string? nick = null;
string name = nick;             // ✗
Console.WriteLine(nick.Length); // ✗
TEXT
warning CS8600: Converting null literal or possible null value to non-nullable type.
warning CS8602: Dereference of a possibly null reference.

Что означают два предупреждения:

  • CS8600 — возможный null попадает в переменную типа без ?;
  • CS8602 — программа обращается к члену через переменную, в которой может быть null.

Это предупреждения, а не ошибки: программа компилируется и запускается. Но третья строка тут же останавливает её исключением NullReferenceException, о котором и предупреждал CS8602. Предупреждение пропадает, когда компилятор видит проверку: внутри if (nick != null) обращаться к nick.Length можно.

int? или string?

Этот вопрос часто задают на собеседовании: чем int? отличается от string?? Ответ из двух частей:

  • int? — отдельный тип Nullable<int>: структура со значением и флагом HasValue, которая есть и во время работы программы;
  • string? — тот же тип string с пометкой для компилятора: при запуске разницы между string и string? нет, а о null предупреждает только компилятор.

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

Value прочитали, не проверив, есть ли значение:

C#
int? score = null;
Console.WriteLine(score.Value); // ✗
TEXT
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.

Компилятор предупреждает об этой строке (CS8629: Nullable value type may be null.), но программу компилирует. Когда программа запускается, Value у пустого int? останавливает её исключением InvalidOperationException. Перед Value проверьте HasValue или возьмите score ?? 0.

Главное

  • int? — значимый тип, который может хранить null: HasValue говорит, есть ли значение, а Value его читает.
  • Value у пустого int? выбрасывает InvalidOperationException; безопасно читать через ??.
  • string? — пометка для компилятора: он выдаёт предупреждения CS8600 и CS8602, но программа компилируется.
  • int? — отдельный тип Nullable<int>, а string и string? при запуске одинаковы.