Два знака вопроса в C#: не ошибка, а мощный инструмент
Представьте: вы пишете код на C#, и вдруг видите конструкцию ??. Первая мысль — опечатка? Лишний знак? На самом деле, это один из самых элегантных и полезных операторов языка — null-объединение (null-coalescing operator). Если вы когда-нибудь ловили NullReferenceException и проклинали всё на свете — этот оператор станет вашим спасательным кругом.
В этой статье мы разберём, что означают два вопросительных знака в C#, как их правильно использовать, чем они отличаются от тернарного оператора, и покажем реальные примеры из практики. Поехали!
Что такое оператор ?? в C#?
Оператор ?? (два вопросительных знака) — это бинарный оператор, который возвращает левый операнд, если он не равен null; в противном случае возвращает правый операнд. Проще говоря: «Если слева не null — бери слева, иначе бери справа».
Синтаксис предельно прост:
var result = left ?? right;
Где:
left— значение, которое может быть null;right— значение по умолчанию (не может быть null, если тип справа — nullable).
Первый пример: спасаемся от null
Допустим, у вас есть переменная string? name (nullable string). Вы хотите вывести имя пользователя, а если его нет — «Гость»:
string? name = GetUserName(); // может вернуть null
string displayName = name ?? "Гость";
Console.WriteLine(displayName);
Без оператора ?? вам пришлось бы писать:
string displayName = (name != null) ? name : "Гость";
Согласитесь, первый вариант читается как обычное предложение: «имя или гость». Красиво, правда?
Оператор ??=: новое поколение null-объединения
Начиная с C# 8.0, появился оператор ??= — это сокращённая форма присваивания с null-объединением. Он присваивает значение правого операнда левому, только если левый операнд равен null.
Синтаксис:
left ??= right;
Это эквивалентно:
if (left == null) left = right;
Пример с ??=
List<int>? numbers = null;
numbers ??= new List<int>(); // numbers теперь = new List<int>()
numbers.Add(42);
Console.WriteLine(numbers.Count); // 1
Обратите внимание: если бы numbers уже был инициализирован (не null), то присваивания бы не произошло. Это идеально для ленивой инициализации.
Сравнение с тернарным оператором (? :)
Многие новички путают ?? с тернарным оператором ? :. Давайте раз и навсегда разберёмся:
- Тернарный оператор — это сокращённая форма if-else. Он проверяет ЛЮБОЕ булево условие:
условие ? значение_если_true : значение_если_false. - Оператор ?? — проверяет ТОЛЬКО на null. Это частный случай, заточенный под одну задачу.
Пример тернарного оператора:
int age = 18;
string status = (age >= 18) ? "Взрослый" : "Несовершеннолетний";
А вот что будет, если попытаться заменить ?? на тернарный:
string? name = null;
string displayName = name != null ? name : "Гость"; // работает, но длиннее
string displayName2 = name ?? "Гость"; // короче и понятнее
Вывод: используйте ??, когда нужно просто подставить значение по умолчанию вместо null. Для всех остальных условий — тернарный оператор.
Цепочка операторов ??
Вы можете объединять несколько операторов ?? в цепочку. Это удобно, когда есть несколько возможных источников значения:
string? firstChoice = GetFromCache();
string? secondChoice = GetFromDatabase();
string? thirdChoice = GetDefault();
string result = firstChoice ?? secondChoice ?? thirdChoice ?? "Ничего не найдено";
Оператор будет последовательно проверять каждое значение: как только найдёт не-null — остановится. Если все null — вернёт самую правую часть (в нашем случае "Ничего не найдено").
Типы и ограничения
Оператор ?? работает с любыми nullable-типами: как со ссылочными (string, классы), так и со значимыми (int?, bool?, DateTime?). Однако есть важное правило: тип правого операнда должен быть неявно преобразуем к типу левого.
Пример с nullable int:
int? a = null;
int b = a ?? 10; // OK: int? может быть преобразован в int
Console.WriteLine(b); // 10
А вот так нельзя:
string? s = null;
int x = s ?? 0; // Ошибка компиляции: нельзя преобразовать string в int
Реальные сценарии использования
1. Параметры методов
public void ProcessData(string? input = null)
{
string data = input ?? "default";
// ...
}
2. Конфигурация
string? connectionString = config["ConnectionString"];
string finalConnection = connectionString ?? "Server=localhost;Database=master;";
3. LINQ и коллекции
var firstItem = list?.FirstOrDefault() ?? "Пустой список";
4. Ленивая инициализация с ??=
private List<string>? _cache;
public List<string> Cache
{
get
{
_cache ??= LoadFromDatabase();
return _cache;
}
}
Частые ошибки и подводные камни
Даже с таким простым оператором можно ошибиться. Вот что нужно помнить:
- Не путайте с тернарным оператором.
??проверяет только на null, а не на любое условие. - Правый операнд вычисляется лениво. Если левый операнд не null, правый даже не будет вычислен. Это важно для производительности.
- Не используйте ?? для значимых типов, которые не nullable. Например,
int a = 5 ?? 10— ошибка, потому чтоintне может быть null. - Будьте осторожны