Menu

Nullable в C#: int?, оператор ??, ?. и проверки на null

Всё о null в C#: типы значений, допускающие null (int?, Nullable<T>), с HasValue, Value и GetValueOrDefault, операторы объединения с null ?? и ??=, null-условный оператор ?., проверки на null и ссылочные типы, допускающие null (string?), из C# 8.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

null означает «нет значения». Ссылочные типы (классы, строки, массивы) всегда могли его хранить. Типы значений (int, decimal, bool, DateTime, структуры) не могут, если не сделать их допускающими null с помощью ?. Для работы со значениями, которых может не быть, C# даёт небольшой набор операторов: ??, ??= и ?..

Типы значений, допускающие null: int?

Добавьте ? к типу значения, чтобы разрешить null. int? это сокращение для структуры Nullable<int>, которая хранит значение плюс флаг, есть ли это значение:

Вывод:

False
True
True
5.50
0
3
Rated 4 stars
4

Типичные источники типов значений, допускающих null, это необязательные поля форм, параметры запроса и столбцы NULL в базе данных: Order с DateTime? ShippedAt не отправлен, пока значение равно null. Альтернатива, «магическое» значение вроде -1 или DateTime.MinValue, выглядит как настоящие данные, и рано или поздно его так и используют.

int? преобразуется в int только явно, потому что преобразование может завершиться неудачей. Это подводит нас к единственному исключению, которое выбрасывают типы значений, допускающие null.

«Nullable object must have a value»

Чтение .Value, когда значения нет, или приведение int?, равного null, к int выбрасывает InvalidOperationException:

Вывод:

InvalidOperationException: Nullable object must have a value.
0
0
not stocked

stock is int count это шаблон: он проверяет наличие значения и распаковывает его в count за один шаг, что читается лучше, чем HasValue плюс Value. Больше форм на странице о сопоставлении с образцом.

Арифметика и сравнения с null

Операторы для типов значений, допускающих null, подняты (lifted): если любой из операндов равен null, результат равен null. Исключение составляют сравнения порядка (<, >, <=, >=): они возвращают false, а не null. (Другое исключение bool? с & и |, где null & false равно false.) Например:

Вывод:

True
[]
False
False
True
True

И a > 5, и a <= 5 ложны, поэтому !(a > 5) не то же самое, что a <= 5, когда a равно null. Код вроде if (!(age >= 18)) Deny(); считает отсутствующий возраст несовершеннолетием, и это может быть не то, что вы имели в виду. Сначала решите, что должен означать null, обычно через ??, а потом сравнивайте.

Оператор объединения с null ??

a ?? b даёт a, когда a не null, и b в противном случае. Он работает с типами значений, допускающими null, и со ссылочными типами и объединяется в цепочки слева направо:

Вывод:

dark
16px
20
ANONYMOUS

Правая часть вычисляется только при необходимости, поэтому cache ?? LoadFromDatabase() не обращается к базе данных, когда в кэше есть значение. ?? принимает и выражение throw, что даёт компактную проверку (см. throw):

this.repository = repository ?? throw new ArgumentNullException(nameof(repository));

??= (C# 8)

x ??= y присваивает y переменной x, только если x равно null. Это идиома ленивой инициализации:

private List<string> tags;

public void AddTag(string tag)
{
    tags ??= new List<string>();   // C# 8 and later
    tags.Add(tag);
}

До C# 8 пишите tags = tags ?? new List<string>(); или if (tags == null) tags = new List<string>();.

Null-условный оператор ?.

a?.Member вычисляет Member, только если a не null; иначе всё выражение равно null. Он превращает цепочку проверок на null в одно выражение:

Вывод:

Braga
(no city)
(no customer)
0
True
handler: saved

Детали, которые стоит знать:

  • Оператор прерывает остаток цепочки: в nobody?.Address?.City, как только nobody оказывается null, ничего правее не выполняется.
  • Когда член возвращает тип значения, результат становится допускающим null: Orders?.Count имеет тип int?, поэтому нужен ?? 0, чтобы снова получить int.
  • До C# 14 ?. не может быть целью присваивания: customer?.Name = "x" не компилируется в более ранних версиях.
  • handler?.Invoke(...) это стандартный способ вызвать событие или необязательный обратный вызов.

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

Проверка на null

if (customer == null) { ... }             // classic
if (customer is null) { ... }             // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... }         // C# 9
if (customer is { } c) { ... }            // C# 8 property pattern: not null, and named

is null всегда выполняет настоящую проверку на null, а == вызывает перегрузку оператора типа, если она есть, поэтому плохо написанная перегрузка может заставить x == null лгать. Для аргументов, которые не должны быть null, падайте сразу:

public void Ship(Order order)
{
    ArgumentNullException.ThrowIfNull(order);   // .NET 6+
    // ...
}

Ссылочные типы, допускающие null (C# 8)

Добавление ? к типу значения меняет тип. Добавление ? к ссылочному типу, начиная с C# 8, меняет только то, что проверяет компилятор. Когда возможность включена (<Nullable>enable</Nullable> в файле проекта, по умолчанию для новых проектов начиная с .NET 6), string означает «никогда не должен быть null», а string? означает «может быть null», и компилятор предупреждает, когда код этому не соответствует:

#nullable enable

public class User
{
    public string Email { get; set; }      // warning CS8618: Non-nullable property 'Email'
                                           // must contain a non-null value when exiting constructor.
    public string? Nickname { get; set; }  // allowed to be null

    public int NicknameLength() => Nickname.Length;
    // warning CS8602: Dereference of a possibly null reference.

    public int SafeLength() => Nickname?.Length ?? 0;   // no warning
}

string? input = Console.ReadLine();
string name = input;                       // warning CS8600: Converting null literal or possible
                                           // null value to non-nullable type.
string checkedName = input ?? "guest";     // no warning
int len = input!.Length;                   // ! tells the compiler "trust me": no warning

Компилятор отслеживает ваши проверки по всему методу: после if (input != null) переменная input считается не null. Оператор ! (подавление null) заглушает предупреждение, когда вы знаете больше, чем анализ; каждый такой оператор это утверждение, которое компилятор не может проверить, поэтому пусть их будет немного.

Два факта снимают большую часть путаницы. Во-первых, это только предупреждения: программа работает так же, и string всё ещё может содержать null во время выполнения, если он пришёл из кода без аннотаций. Во-вторых, string? это не Nullable<string>; у него нет HasValue.

Частые ошибки

  • .Value без проверки. Выбрасывает InvalidOperationException, когда значение null. Используйте ??, GetValueOrDefault() или is int n.
  • Предположение, что !(x > 5) означает x <= 5. Не когда x равно null: любое сравнение порядка с null ложно.
  • Закрытие каждого null через ?.. Это глушит симптом и уводит сбой дальше от причины.
  • Отношение к предупреждениям о ссылочных типах, допускающих null, как к шуму. Каждое отмечает строку, которая может выбросить NullReferenceException. Исправьте поток выполнения или выразите намерение через ?.
  • Магические значения вместо null. -1, 0 и DateTime.MinValue для «неизвестно» выглядят как настоящие данные. Используйте int? и DateTime?.

Часто задаваемые вопросы

Что означает int? в C#?

int? это сокращение для Nullable<int>: int, который может быть и null. Типы значений вроде int, decimal, bool и DateTime обычно всегда содержат значение; добавление ? даёт им состояние «нет значения», которое нужно для незаполненного поля формы или столбца NULL в базе данных. Проверяйте его через HasValue или != null, а читайте через Value или GetValueOrDefault().

Что делает ?? в C#?

a ?? b это оператор объединения с null: он даёт a, если a не null, иначе b. string name = input ?? "guest"; подставляет запасное значение одним выражением. Он объединяется в цепочки (a ?? b ?? c берёт первое значение, отличное от null), а b вычисляется, только когда нужно. ??= (C# 8) присваивает правую часть, только если переменная равна null.

Что делает ?. в C#?

a?.B это null-условный оператор: если a равно null, всё выражение равно null, а B не вычисляется, поэтому NullReferenceException не возникает. Он объединяется в цепочки (order?.Customer?.Address?.City) и работает для вызовов методов (logger?.Log(...)), событий (Changed?.Invoke(...)) и индексаторов (items?[0]). Сочетайте его с ?? для значения по умолчанию: order?.Customer?.Name ?? "unknown".

Что вызывает ошибку «Nullable object must have a value»?

Чтение .Value у типа значения, допускающего null, который равен null, или приведение его к типу без null ((int)maybe) выбрасывает InvalidOperationException с этим сообщением. Сначала проверьте HasValue или используйте GetValueOrDefault(), ?? fallback или шаблон: if (maybe is int n) { ... }.

Как проверить на null в C#?

Работают и x == null, и x is null; на is null (C# 7) не может повлиять тип, перегружающий ==, поэтому многие кодовые базы предпочитают его. Отрицание это x != null или x is not null в C# 9. Чтобы отвергать аргументы null, используйте ArgumentNullException.ThrowIfNull(x) (.NET 6) или _ = x ?? throw new ArgumentNullException(nameof(x)); в старом коде.

Что такое ссылочные типы, допускающие null, в C#?

Возможность компилятора C# 8, включаемая через <Nullable>enable</Nullable> (по умолчанию в новых проектах .NET 6+). Когда она включена, string означает «никогда не null», а string? означает «может быть null», и компилятор предупреждает, когда вы можете разыменовать null (CS8602) или присвоить null переменной, не допускающей null (CS8600). Меняются только предупреждения, а не поведение во время выполнения.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ