Gdy dane z konfiguracji, bazy albo żądania HTTP mogą mieć wartość null, potrzebujesz bezpiecznego sposobu na ustawienie wartości domyślnej. Operator ??, znany w anglojęzycznych materiałach jako c# double question mark, rozwiązuje ten problem jednym czytelnym wyrażeniem. Pokażę, jak działa, czym różni się od ?, ?. i operatora trójargumentowego oraz gdzie łatwo popełnić błąd.
Operator ?? szybko zamienia null na bezpieczną wartość domyślną
-
Składnia
lewaWartość ?? wartośćDomyślnazwraca lewą stronę, gdy nie jest równanull. - Krótki obwód prawa strona jest obliczana tylko wtedy, gdy lewa wartość faktycznie jest pusta.
-
Nie zastępuje pustego tekstu wartości
"",0ifalsenie są traktowane jaknull. -
Operator ??= przypisuje wartość domyślną tylko wtedy, gdy zmienna ma
null. -
Typy nullable operator dobrze współpracuje z
string?i typami wartości takimi jakint?.

Co robi operator ?? w C#
Najprościej czytać go jako „użyj lewej wartości, a jeśli jej nie ma, wybierz prawą”. Przykład string name = userName ?? "Gość"; oznacza, że do name trafi zawartość userName, jeśli nie jest równa null, albo tekst „Gość” w przeciwnym przypadku.
string? userName = null;
string displayName = userName ?? "Gość";
Console.WriteLine(displayName); // GośćW klasycznym zapisie ten sam kod wyglądałby mniej zwięźle i wymagałby instrukcji if. Sam najczęściej stosuję ?? przy wartościach opcjonalnych, które mają rozsądną wartość zastępczą, na przykład przy ustawieniach aplikacji, nazwach wyświetlanych użytkownikowi albo parametrach zapytania.
string displayName;
if (userName is not null)
{
displayName = userName;
}
else
{
displayName = "Gość";
}Operator jest wyrażeniem, więc można używać go bezpośrednio w przypisaniu, zwracanej wartości, interpolacji tekstu lub wywołaniu metody. To ważne praktycznie, bo nie tworzysz osobnej zmiennej tylko po to, aby wykonać prostą obsługę braku danych.
return configuration["ApiUrl"] ?? "https://localhost:5001";
logger.LogInformation(
"Użytkownik: {Name}",
userName ?? "anonimowy");Jak działa sprawdzanie null
Operator ?? sprawdza wyłącznie, czy lewa strona ma wartość null. Nie uzna za brak danych pustego tekstu, liczby 0 ani wartości logicznej false. To jedna z najczęstszych pomyłek, szczególnie gdy dane pochodzą z formularza lub zewnętrznego API.
string? title = "";
string result = title ?? "Brak tytułu";
Console.WriteLine(result); // pusty tekst, nie "Brak tytułu"Jeżeli chcesz potraktować pusty albo złożony wyłącznie ze spacji tekst jako brak wartości, potrzebujesz osobnego warunku. W takim przypadku czytelne rozwiązanie wygląda tak:
string? title = " ";
string result = string.IsNullOrWhiteSpace(title)
? "Brak tytułu"
: title;Istotne jest też leniwe obliczanie prawej strony. Metoda lub wyrażenie po prawej stronie zostanie wykonane tylko wtedy, gdy lewa wartość jest równa null. Dzięki temu można bezpiecznie używać kosztowniejszych operacji jako wartości rezerwowej.
string? cachedValue = GetFromCache();
string value = cachedValue ?? LoadFromDatabase();Jeśli GetFromCache() zwróci tekst, LoadFromDatabase() nie zostanie wywołane. Ta cecha ma znaczenie nie tylko dla wydajności, ale także dla skutków ubocznych, na przykład dodatkowego zapytania do bazy lub zapisu w logach.
Łączenie kilku wartości domyślnych
Operatory ?? można łączyć, aby sprawdzać kilka źródeł danych po kolei. Wyrażenie jest analizowane od lewej strony i kończy się na pierwszej wartości, która nie jest null.
string? commandLineValue = null;
string? environmentValue = "Production";
string? appSettingsValue = "Development";
string environment =
commandLineValue
?? environmentValue
?? appSettingsValue
?? "Unknown";W tym przykładzie wybrana zostanie wartość „Production”. Taki zapis dobrze pasuje do konfiguracji aplikacji .NET, gdzie ustawienie może pochodzić z argumentów uruchomieniowych, zmiennej środowiskowej, pliku konfiguracyjnego albo wartości awaryjnej.
Operator jest prawostronnie łączny, więc zapis a ?? b ?? c działa jak a ?? (b ?? c). Zwykle nie trzeba dodawać nawiasów, ale przy dłuższych wyrażeniach warto je zastosować, gdy poprawiają czytelność kodu.
Nie używałbym jednak długiego łańcucha jako sposobu na ukrywanie niejasnych reguł biznesowych. Jeżeli każda wartość ma inne znaczenie, lepiej nazwać decyzję osobną metodą albo rozbić ją na kilka kroków. Zwięzłość nie powinna utrudniać zrozumienia priorytetów konfiguracji.
Różnica między ??, ?., ? i operatorem trójargumentowym
Znaki zapytania występują w C# w kilku różnych rolach. Nie są wymienne, dlatego warto rozdzielić ich znaczenie, zanim zaczniesz stosować je automatycznie.
| Element | Zastosowanie | Przykład |
|---|---|---|
?? |
Wybiera wartość zastępczą, gdy wynik jest null. |
name ?? "Gość" |
?. |
Bezpiecznie odwołuje się do członka, gdy obiekt może być pusty. | user?.Profile |
? przy typie |
Oznacza typ nullable, który może przechowywać null. |
int? |
?: |
Wybiera wynik na podstawie dowolnego warunku logicznego. | age >= 18 ? "pełnoletni" : "niepełnoletni" |
Operatory ?. i ?? często występują razem. Pierwszy bezpiecznie pobiera właściwość, a drugi ustawia wartość, jeśli całe wyrażenie zakończy się wynikiem null.
string? city =
user?.Address?.City
?? "Nieznane miasto";Ten zapis chroni przed NullReferenceException, jeśli nie istnieje użytkownik, adres albo miasto. Trzeba jednak pamiętać, że nie sprawdza pustego tekstu. Jeśli właściwość City ma wartość "", wynik nadal będzie pustym tekstem.
Operator trójargumentowy jest lepszy, gdy warunek nie dotyczy samego null. Przykładowo sprawdzenie pełnoletności, zakresu liczby lub statusu obiektu nie powinno być zastępowane przez ??, bo oba mechanizmy rozwiązują różne problemy.
Typy nullable i zgodność typów
W przypadku typów referencyjnych, takich jak string czy własne klasy, operator działa naturalnie z wartościami oznaczonymi jako nullable. Włączona analiza nullable w nowoczesnych projektach C# pomaga kompilatorowi wskazać miejsca, w których potencjalnie używasz null.
string? input = GetInput();
string text = input ?? "domyślna wartość";Po prawej stronie musi znaleźć się wartość zgodna z typem wyniku albo możliwa do niego niejawnie przekonwertowania. Przykład z typem wartości wygląda tak:
int? optionalNumber = GetNumber();
int number = optionalNumber ?? 10;Zmiennej number zostanie przypisana liczba z optionalNumber albo 10. To wygodniejsza forma niż ręczne sprawdzanie właściwości HasValue i pobieranie Value.
Nie można natomiast użyć operatora w taki sposób:
int number = 5 ?? 10;Lewa strona ma tutaj zwykły, niepusty typ wartości int, więc sprawdzanie null nie ma sensu. Kompilator zgłosi błąd, ponieważ nie każdy typ może przyjąć wartość pustą.
Podobny problem pojawia się przy niezgodnych typach po obu stronach. Gdy domyślna wartość ma być innego rodzaju, trzeba jawnie wykonać konwersję albo zmienić typ wyniku. W praktyce najłatwiej sprawdzić deklaracje obu operandów i nie zakładać, że C# zawsze sam dopasuje typ.
Operator ??= i przypisanie tylko w razie potrzeby
Wariant ??= służy do przypisania wartości domyślnej bez tworzenia osobnego wyrażenia. Zadziała tylko wtedy, gdy zmienna po lewej stronie jest równa null.
string? token = null;
token ??= GenerateToken();
Console.WriteLine(token);Jeżeli token już zawiera tekst, metoda GenerateToken() nie zostanie wywołana i istniejąca wartość pozostanie bez zmian. To wygodny wzorzec przy leniwym inicjalizowaniu pól, pamięci podręcznej albo domyślnych ustawień.
private List? _items;
public List Items
{
get
{
_items ??= LoadItems();
return _items;
}
} W starszym zapisie trzeba byłoby sprawdzić pole instrukcją if. Operator skraca kod, ale nie powinien być używany tam, gdzie inicjalizacja ma ważne skutki uboczne i powinna być widoczna na pierwszy rzut oka.
Warto odróżnić ??= od zwykłego przypisania. Instrukcja value = defaultValue zawsze nadpisuje zawartość, natomiast value ??= defaultValue robi to wyłącznie dla wartości pustej.
Typowe błędy przy używaniu podwójnego znaku zapytania
Oczekiwanie, że pusty tekst zostanie zastąpiony
To najczęstsze nieporozumienie. Dla operatora ?? pusty tekst jest prawidłową wartością, podobnie jak 0 i false. Gdy aplikacja ma rozróżniać „brak danych” od „świadomie podanej wartości pustej”, takie zachowanie jest wręcz potrzebne.
Ukrywanie błędnej konfiguracji
Wartość domyślna jest dobrym zabezpieczeniem tylko wtedy, gdy ma sens biznesowy. Dla opcjonalnego podpisu użytkownika użycie "Gość" jest rozsądne, ale dla brakującego klucza API lepiej zgłosić błąd niż po cichu użyć przypadkowej wartości.
string connectionString =
configuration["ConnectionStrings:Main"]
?? throw new InvalidOperationException(
"Brak głównego connection stringa.");Takie rozwiązanie pozwala aplikacji zakończyć start z jasnym komunikatem. Sam preferuję tę formę dla ustawień wymaganych, ponieważ szybciej ujawnia problem niż odległy wyjątek podczas pierwszego użycia usługi.
Mylenie ?? z operatorem !
Operator ! przy wyrażeniu, na przykład value!, wycisza ostrzeżenie analizy nullable. Nie dostarcza wartości domyślnej i nie sprawdza w czasie działania, czy obiekt nie jest pusty. Jeśli obiekt faktycznie okaże się równy null, błąd nadal może wystąpić.
Przeczytaj również: Pętla do-while w C# - składnia, przykłady i pułapki
Przesadne skracanie logiki
Wyrażenie settings?.User?.Profile?.Name ?? "Gość" jest czytelne, gdy opisuje prostą ścieżkę danych. Jeśli jednak po drodze dochodzą reguły uprawnień, walidacja i kilka wyjątków, lepiej rozdzielić je na nazwane operacje. Operator ma upraszczać obsługę braku wartości, a nie zastępować całą logikę aplikacji.
Jak podejmuję decyzję o użyciu ?? w projekcie
Najpierw rozstrzygam, czy null jest poprawnym stanem, czy oznacza błąd. Jeśli brak danych jest normalny i istnieje bezpieczna wartość zastępcza, wybieram ??. Jeśli brak wartości wskazuje na źle skonfigurowaną aplikację, stosuję ?? throw albo walidację przy starcie.
- Użyj
??, gdy potrzebujesz wartości domyślnej dla opcjonalnych danych. - Użyj
?., gdy chcesz bezpiecznie przejść przez obiekt, który może być pusty. - Użyj
string.IsNullOrWhiteSpace, gdy pusty lub biały tekst ma znaczyć brak danych. - Użyj
??=, gdy chcesz zainicjalizować wartość tylko raz. - Użyj wyjątku lub walidacji, gdy wartość jest wymagana do działania aplikacji.
W codziennym kodzie .NET najwięcej korzyści daje połączenie ?. z ?? oraz świadome rozróżnienie między null a pustą wartością. Sam traktuję ten operator jako małe narzędzie, które robi dużą różnicę w czytelności, ale tylko wtedy, gdy wartość domyślna rzeczywiście opisuje poprawne zachowanie programu.
