Операторы null в C#

Подсказка

Эта статья является частью раздела "Основы" для разработчиков , которые знают по крайней мере один язык программирования и учат C#. Если вы не знакомы с программированием, сначала начните с учебных пособий по началу работы. Для полного справочника по операторам см. раздел о операторах доступа к членам и операторах объединения null в справочнике по языку.

C# предоставляет несколько операторов, которые обеспечивают краткий null-безопасный код. Вместо использования вложенных условных операторов if (x != null) в коде, эти операторы позволяют выразить доступ с проверкой null, значения по умолчанию и проверки на null в одном выражении.

В этой статье рассматриваются ?. и ?[] для условного доступа с null, ?? для объединения null, ??= для присваивания с объединением null, а также is null/is not null для сопоставления null-шаблонов.

Доступ к члену с null-условием ?.

Оператор ?. обращается к члену только в том случае, если объект не имеет значения NULL. Если объект имеет значение null, все выражение вычисляется в null, вместо того чтобы выдать NullReferenceException:

string? name = null;

// Without ?., accessing a member on null throws NullReferenceException:
// int len = name.Length; // throws if name is null

// ?. returns null instead of throwing:
int? len = name?.Length;
Console.WriteLine(len.HasValue); // False

name = "C#";
Console.WriteLine(name?.Length); // 2

Оператор ?.укороченное вычисление: когда левая часть равна null, все справа пропускается. Вызовы метода не выполняются и побочные эффекты не возникают.

Вы можете связать несколько ?. операторов в одном выражении. Цепочка останавливается при обнаружении первого null.

string? input = null;

// Chain ?. across multiple method calls — short-circuits at the first null:
string? upper = input?.Trim()?.ToUpperInvariant();
Console.WriteLine(upper ?? "(none)"); // (none)

input = "  hello  ";
Console.WriteLine(input?.Trim()?.ToUpperInvariant()); // HELLO

Доступ к индексатору NULL ?[]

Оператор ?[] применяет тот же механизм сокращённого вычисления к индексатору и доступу к массиву. Используйте его, если сама коллекция может быть null:

string[]? tags = null;

// ?[] accesses an element only when the collection is non-null
string? first = tags?[0];
Console.WriteLine(first ?? "(none)"); // (none)

tags = ["csharp", "dotnet", "nullable"];
Console.WriteLine(tags?[0]);          // csharp

Цепные null-условные операторы

Используйте цепочку нескольких операторов ?., чтобы обойти путь с потенциально пустыми ссылками. Цепь закорачивается на первом null:

var order = new Order("ORD-001", null);

// Each ?. short-circuits when null: Customer is null, so Address and City are never accessed
string? city = order.Customer?.Address?.City;
Console.WriteLine(city ?? "(no city)"); // (no city)

var fullOrder = new Order("ORD-002",
    new Customer("Alice", new Address("123 Main St", "Springfield", "IL")));

Console.WriteLine(fullOrder.Customer?.Address?.City); // Springfield

Если Customer равно null, ни Address, ни City не вычисляются. Возвращается целое выражение null.

Потокобезопасный вызов делегата

?. предоставляет чистый, потокобезопасный способ вызова делегата или вызова события. Выражение делегата вычисляется только один раз, поэтому нет окна для другого потока отмены подписки между проверкой NULL и вызовом:

EventHandler? clicked = null;

// No subscribers — ?.Invoke does nothing instead of throwing NullReferenceException
clicked?.Invoke(null, EventArgs.Empty);

clicked += (_, _) => Console.WriteLine("Button clicked!");

// With a subscriber — ?.Invoke calls the handler
clicked?.Invoke(null, EventArgs.Empty);
// Output: Button clicked!

Этот шаблон заменяет старый if (clicked != null) clicked(...) идиом.

Объединение с значением NULL ??

Оператор ?? возвращает свой левый операнд, когда он не имеет значения NULL, и его правый операнд, когда слева null. Используйте его для предоставления значения по умолчанию:

string? username = null;

// ?? returns the right-hand value when the left-hand is null
string display = username ?? "Guest";
Console.WriteLine(display); // Guest

username = "alice";
display  = username ?? "Guest";
Console.WriteLine(display); // alice

?? является правым ассоциативным, поэтому a ?? b ?? c интерпретируется как a ?? (b ?? c). Первое значение, отличное от NULL, выигрывает. Распространенный шаблон заключается в использовании связи ?. с ??: сначала используется ?. для безопасного обхода потенциальной цепочки null, а затем ?? для замены на значение по умолчанию, если цепочка вернула null. Полный пример см. в разделе "Объединение операторов NULL".

Операция присваивания при null-слиянии ??=

Оператор ??= назначает правое значение переменной только в том случае, если переменная имеет значение null. Используйте его для отложенной инициализации:

List<string>? cache = null;

// ??= assigns only when the variable is null
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

// cache is already non-null, so LoadData() isn't called again
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

static List<string> LoadData() => ["alpha", "beta", "gamma"];

Выражение справа вычисляется только в том случае, если переменная имеет значение null. Если переменная уже имеет значение, правая часть не вычисляется.

Условное назначение null (C# 14)

Начиная с C# 14, можно использовать ?. и ?[] в качестве целевых объектов назначения. Назначение выполняется только в том случае, если левый объект не имеет значения NULL:

AppConfig? config = new AppConfig();

// Assigns only when config is non-null (C# 14)
config?.Theme = "dark";
Console.WriteLine(config?.Theme); // dark

AppConfig? missing = null;
missing?.Theme = "light";                         // no-op: missing is null
Console.WriteLine(missing?.Theme ?? "(no config)"); // (no config)

Правая сторона вычисляется только в том случае, если известно, что левая сторона не является NULL.

Сопоставление шаблонов NULL: is null и is not null

Шаблоны is null и is not null проверяют, является ли выражение null:

string? input = null;

// is null is the preferred test — unaffected by operator overloading
if (input is null)
{
    Console.WriteLine("No input provided.");
}

// == null also works, but a custom == operator can change its behavior
if (input == null)
{
    Console.WriteLine("Still no input.");
}

Предпочитайте is null вместо == null для проверки на NULL. Оператор == может быть перегружен, то есть x == null может возвратить true даже в том случае, если x не null, когда тип определяет настраиваемый оператор равенства. Шаблон is null всегда проверяет фактическую ссылку null независимо от перегрузки оператора.

string? value = "hello";

if (value is not null)
{
    Console.WriteLine(value.ToUpper()); // HELLO
}

Комбинирование операторов NULL

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

Order? order = GetPendingOrder();

// Chain ?. for safe traversal, ?? for a fallback, is null for a clear guard
string city = order?.Customer?.Address?.City ?? "unknown";

if (order is null)
{
    Console.WriteLine("No pending order.");
}
else
{
    Console.WriteLine($"Shipping to: {city}");
}
// Output: No pending order.

Оператор, игнорирующий null !

Оператор ! postfix подавляет предупреждения, допускающие значение NULL. Добавьте, ! чтобы сообщить компилятору, что это выражение определенно не равно NULL. Оператор не действует во время выполнения. Это влияет только на анализ состояния null компилятора.

string? name = FindUser("alice");

// Use ! only when you have information the compiler doesn't.
// FindUser guarantees a non-null result for known usernames.
int length = name!.Length;
Console.WriteLine(length); // 5

Используйте ! экономно, и только в тех случаях, когда у вас есть информация, которой нет у компилятора. Примеры включают тесты, которые намеренно используют null для проверки логики аргументации, или вызов метода, контракт которого гарантирует ненулевой возврат для заданного входного значения. Чрезмерное использование ! сводит на нет цель nullable ссылочных типов. Полное объяснение см. в разделе " Ссылочные типы, допускающие значение NULL".

См. также