Два знака вопроса в C#: что это и как использовать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Два знака вопроса в 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.
  • Будьте осторожны

Блоги

Рекомендаций по книгам