Menu

Nullable w C#: int?, operator ??, ?. i sprawdzanie null

Wszystko o null w C#: typy wartościowe nullable (int?, Nullable<T>) z HasValue, Value i GetValueOrDefault, operatory ?? i ??=, operator warunkowy ?., sprawdzanie null i nullable reference types (string?) z C# 8.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

null oznacza "brak wartości". Typy referencyjne (klasy, stringi, tablice) zawsze mogły go przechowywać. Typy wartościowe (int, decimal, bool, DateTime, struktury) nie mogą, chyba że uczynisz je nullable za pomocą ?. C# daje wtedy mały zestaw operatorów do pracy z wartościami, których może brakować: ??, ??= i ?..

Typy wartościowe nullable: int?

Dodaj ? do typu wartościowego, żeby dopuścić null. int? to skrót od struktury Nullable<int>, która przechowuje wartość i flagę mówiącą, czy ta wartość jest obecna:

Wynik:

False
True
True
5.50
0
3
Rated 4 stars
4

Typowe źródła typów wartościowych nullable to opcjonalne pola formularzy, parametry zapytań i kolumny NULL w bazie danych: Order z DateTime? ShippedAt nie został wysłany, dopóki wartość jest null. Alternatywa, czyli magiczna wartość w rodzaju -1 albo DateTime.MinValue, wygląda jak prawdziwe dane i prędzej czy później zostanie tak użyta.

int? konwertuje się na int tylko jawnie, bo konwersja może się nie udać. To prowadzi nas do jedynego wyjątku, który rzucają typy wartościowe nullable.

"Nullable object must have a value"

Odczyt .Value, gdy wartości nie ma, albo rzutowanie int? równego null na int rzuca InvalidOperationException:

Wynik:

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

stock is int count to wzorzec: w jednym kroku sprawdza, czy jest wartość, i rozpakowuje ją do count, co czyta się lepiej niż HasValue razem z Value. Więcej form znajdziesz na stronie o dopasowywaniu wzorców.

Arytmetyka i porównania z null

Operatory na typach wartościowych nullable są podniesione (lifted): jeśli którykolwiek operand jest null, wynik jest null. Wyjątkiem są porównania porządkowe (<, >, <=, >=): zwracają false zamiast null. (Drugi wyjątek to bool? z & i |, gdzie null & false daje false.) Na przykład:

Wynik:

True
[]
False
False
True
True

Zarówno a > 5, jak i a <= 5 są fałszywe, więc gdy a jest null, !(a > 5) to nie to samo co a <= 5. Kod w rodzaju if (!(age >= 18)) Deny(); traktuje brak wieku jak niepełnoletność, co może, ale nie musi być tym, o co ci chodziło. Najpierw zdecyduj, co ma oznaczać null, zwykle za pomocą ??, a dopiero potem porównuj.

Operator null coalescing ??

a ?? b daje a, gdy a nie jest null, a w przeciwnym razie b. Działa na typach wartościowych nullable i na typach referencyjnych, a łańcuchy obliczane są od lewej do prawej:

Wynik:

dark
16px
20
ANONYMOUS

Prawa strona jest obliczana tylko wtedy, gdy jest potrzebna, więc cache ?? LoadFromDatabase() nie odpytuje bazy danych, gdy cache ma wartość. ?? przyjmuje też wyrażenie throw, co daje zwięzłe zabezpieczenie (zobacz throw):

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

??= (C# 8)

x ??= y przypisuje y do x tylko wtedy, gdy x jest null. To idiom leniwej inicjalizacji:

private List<string> tags;

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

Przed C# 8 pisz tags = tags ?? new List<string>(); albo if (tags == null) tags = new List<string>();.

Operator warunkowy null ?.

a?.Member oblicza Member tylko wtedy, gdy a nie jest null; w przeciwnym razie całe wyrażenie jest null. Zamienia łańcuch sprawdzeń null w jedno wyrażenie:

Wynik:

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

Szczegóły warte poznania:

  • Operator skraca resztę łańcucha: w nobody?.Address?.City, gdy tylko nobody okaże się null, nic po prawej się nie wykonuje.
  • Gdy składowa zwraca typ wartościowy, wynik staje się nullable: Orders?.Count to int?, dlatego potrzebuje ?? 0, żeby znów stać się int.
  • Przed C# 14 ?. nie może być celem przypisania: customer?.Name = "x" nie kompiluje się we wcześniejszych wersjach.
  • handler?.Invoke(...) to standardowy sposób na wywołanie zdarzenia albo opcjonalnego callbacku.

Używaj ?. tam, gdzie null jest prawidłowym stanem. Wstawianie go wszędzie, żeby uniknąć wyjątków, ukrywa błąd, który wytworzył nieoczekiwany null, a program działa dalej z brakującymi danymi, zamiast zatrzymać się tam, gdzie leży problem.

Sprawdzanie 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 zawsze wykonuje prawdziwe sprawdzenie null, a == wywołuje przeciążenie operatora danego typu, jeśli takie istnieje, więc źle napisane przeciążenie może sprawić, że x == null skłamie. Dla argumentów, które nie mogą być null, zgłaszaj błąd wcześnie:

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

Nullable reference types (C# 8)

Dodanie ? do typu wartościowego zmienia typ. Dodanie ? do typu referencyjnego, od C# 8, zmienia tylko to, co sprawdza kompilator. Gdy funkcja jest włączona (<Nullable>enable</Nullable> w pliku projektu, domyślnie w nowych projektach od .NET 6), string oznacza "nigdy nie powinno być null", a string? oznacza "może być null", i kompilator ostrzega, gdy kod do tego nie pasuje:

#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

Kompilator śledzi twoje sprawdzenia w całej metodzie: po if (input != null) zmienna input jest traktowana jako różna od null. Operator ! (null-forgiving) wycisza ostrzeżenie, gdy wiesz więcej niż analiza; każde jego użycie to twierdzenie, którego kompilator nie może zweryfikować, więc stosuj go rzadko.

Dwa fakty, które rozwiewają większość nieporozumień. Po pierwsze, to wyłącznie ostrzeżenia: program działa tak samo, a string nadal może w czasie działania zawierać null, jeśli pochodzi z kodu bez adnotacji. Po drugie, string? to nie Nullable<string>; nie ma na nim HasValue.

Typowe błędy

  • .Value bez sprawdzenia. Rzuca InvalidOperationException, gdy jest null. Użyj ??, GetValueOrDefault() albo is int n.
  • Założenie, że !(x > 5) oznacza x <= 5. Nie wtedy, gdy x jest null: każde porównanie porządkowe z null jest fałszywe.
  • Zasłanianie każdego null operatorem ?.. Wycisza objaw i oddala awarię od przyczyny.
  • Traktowanie ostrzeżeń nullable reference jako szumu. Każde wskazuje linię, która może rzucić NullReferenceException. Popraw przepływ albo wyraź intencję za pomocą ?.
  • Magiczne wartości zamiast null. -1, 0 i DateTime.MinValue jako "nieznane" wyglądają jak prawdziwe dane. Używaj int? i DateTime?.

Najczęściej zadawane pytania

Co oznacza int? w C#?

int? to skrót od Nullable<int>: int, który może też mieć wartość null. Typy wartościowe, takie jak int, decimal, bool i DateTime, normalnie zawsze przechowują wartość; dodanie ? daje im stan "brak wartości", którego potrzebuje puste pole formularza albo kolumna NULL w bazie danych. Sprawdzasz go przez HasValue lub != null, a odczytujesz przez Value lub GetValueOrDefault().

Co robi ?? w C#?

a ?? b to operator null coalescing: daje a, jeśli a nie jest null, a w przeciwnym razie b. string name = input ?? "guest"; podaje wartość zastępczą w jednym wyrażeniu. Można go łączyć w łańcuch (a ?? b ?? c bierze pierwszą wartość różną od null), a b jest obliczane tylko wtedy, gdy trzeba. ??= (C# 8) przypisuje prawą stronę tylko wtedy, gdy zmienna jest null.

Co robi ?. w C#?

a?.B to operator warunkowy null: jeśli a jest null, całe wyrażenie jest null, a B nigdy nie jest obliczane, więc nie ma NullReferenceException. Można go łączyć w łańcuch (order?.Customer?.Address?.City) i działa dla wywołań metod (logger?.Log(...)), zdarzeń (Changed?.Invoke(...)) i indeksatorów (items?[0]). Połącz go z ??, żeby dostać wartość domyślną: order?.Customer?.Name ?? "unknown".

Co powoduje błąd "Nullable object must have a value"?

Odczyt .Value typu wartościowego nullable, który jest null, albo rzutowanie go na typ bez nullable ((int)maybe) rzuca InvalidOperationException z tym komunikatem. Najpierw sprawdź HasValue albo użyj GetValueOrDefault(), ?? fallback lub wzorca: if (maybe is int n) { ... }.

Jak sprawdzić null w C#?

Działają zarówno x == null, jak i x is null; na is null (C# 7) nie może wpłynąć typ, który przeciąża ==, więc wiele projektów je preferuje. Zaprzeczenie to x != null albo x is not null w C# 9. Żeby odrzucić argumenty null, użyj ArgumentNullException.ThrowIfNull(x) (.NET 6) albo w starszym kodzie _ = x ?? throw new ArgumentNullException(nameof(x));.

Czym są nullable reference types w C#?

To funkcja kompilatora z C# 8, włączana przez <Nullable>enable</Nullable> (domyślnie w nowych projektach .NET 6+). Gdy jest włączona, string oznacza "nigdy null", a string? oznacza "może być null", i kompilator ostrzega, gdy możesz odwołać się do null (CS8602) albo przypisać null do zmiennej bez nullable (CS8600). Zmienia tylko ostrzeżenia, a nie zachowanie w czasie działania.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ