1. Wprowadzenie
Wyobraź sobie taką sytuację. Przechowujesz dane o kotach w kodzie. I chcesz oprócz, na przykład, wieku kota, trzymać też stany typu "brak danych", "wiek nieznany". Można by trzymać 0, ale zero to przecież całkiem legitny wiek.
int age = 0; // Wiek kota. Ale co to znaczy 0? To kociak czy "nieznane"?
Problem w tym, że int przyjmuje tylko liczby całkowite, a "brak danych" wśród liczb nie istnieje. Gdyby wiek był stringiem, można by przypisać null. Ale z liczbami taki trik nie przejdzie:
int age = null; // Błąd! Nie można przypisać null do zmiennej typu int
To dlatego, że typy wartościowe (struct-y jak int, double, DateTime, bool) zawsze coś przechowują. Nie mają "pustego" stanu (w przeciwieństwie do typów referencyjnych, gdzie null to po prostu brak obiektu).
Przykład z życia:
Próbując obejść to ograniczenie, programiści czasem wymyślają "specjalne" wartości: np. -1 albo int.MaxValue dla "brak wartości". Ale to jest brzydkie, niewygodne i niebezpieczne: łatwo pomylić prawdziwą wartość z placeholderem.
2. Nullable-typ: typ, który może być "null"
Pomysł
A co, jeśli pozwolić zwykłym liczbom (i w ogóle wszystkim typom wartościowym) przechowywać nie tylko wartość, ale też null?
W C# jest to ogarnięte przez specjalną klasę-wrapper Nullable<T>. Taka klasa może mieć albo wartość typu T, albo nic (czyli null).
Składnia
int? age = null; // Zmienna age może być liczbą albo null
Kompilator C#, gdy widzi taki kod, tak naprawdę traktuje go jak:
Nullable<int> age = new Nullable<int>(); // Zmienna age nie ma wartości
Albo, jeśli chcesz jawnie podać wartość:
Nullable<int> age = new Nullable<int>(42); // age ma wartość 42
Czyli znak zapytania zamienia każdy typ wartościowy w jego nullable-wersję:
- int? (to samo co Nullable<int>)
- double?
- DateTime?
- i dowolne inne struktury
Nullable-wrapery dla najpopularniejszych typów wartościowych
| Zwykły typ | Nullable-typ | Przykład zapisu |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Możesz też napisać dłużej, używając klasy-wrappera:
Nullable<int> pages = null;
Ale pisać int? — krócej, a więc fajniej.
3. Jak używać nullable-typów
Przypisanie i sprawdzanie
Przypisanie null działa tak samo jak z typami referencyjnymi:
Deklaracja i przypisanie
int? temperature = null;
temperature = 25;
Sprawdzanie na null
Jak sprawdzić, czy nasz nullable-typ ma jakąś wartość?
if (temperature != null)
{
Console.WriteLine($"Temperatura: {temperature}");
}
else
{
Console.WriteLine("Brak danych o temperaturze");
}
Właściwości Nullable-typów: .HasValue i .Value
Nullable-typy mają dwa ważne properties:
- .HasValue — zwraca true, jeśli zmienna coś ma (nie null).
- .Value — sama wartość, jeśli jest (inaczej będzie wyjątek).
int? temperature = null;
if (temperature.HasValue) //nie będzie wyjątku!
{
Console.WriteLine($"Temperatura: {temperature.Value}°C");
}
else
{
Console.WriteLine("Temperatura nieznana");
}
Ten kod zadziała: temperature nie jest null, tylko ma null w środku. I zawsze możesz wywołać na typie Nullable property HasValue. Ale próba pobrania .Value, gdy go nie ma — zły pomysł, dostaniesz wyjątek InvalidOperationException. Sprawdzaj obecność wartości wcześniej!
Skrócony zapis wyświetlania
W wielu przypadkach wystarczy po prostu użyć zmiennej nullable-typu — i C# sam ogarnie:
int? hours = null;
Console.WriteLine(hours); // nic nie wyświetla (po prostu pusto)
hours = 10;
Console.WriteLine(hours); // wyświetla 10
4. Operacje i nullable-typy: arytmetyka, porównania, konwersje
Operacje: co się dzieje?
Jeśli jeden z operandów jest nullable, wynik też jest nullable.
A jeśli któryś operand jest null, wynik też null.
int? a = 5;
int? b = null;
int? sum = a + b; // sum == null
int? c = 10;
int? d = 15;
int? total = c + d; // total == 25
Operacje porównania
Możesz porównywać nullable-typy ze zwykłymi liczbami:
int? score = null;
if (score > 0) Console.WriteLine("Świetny wynik!");
else Console.WriteLine("Brak wyniku"); // Ta gałąź się wykona
Jeśli score jest null, warunek score > 0 będzie false.
Jawna i niejawna konwersja
- Ze zwykłego typu do nullable: automatycznie.
- Z nullable do zwykłego: tylko jeśli nie null (inaczej będzie wyjątek).
int? x = 3;
int y = x.Value; // Ok, jeśli x nie jest null
// Niejawnie
int? z = y;
5. Nullable i metody: parametry i wartości zwracane
Zwracanie nullable-wartości
Jeśli metoda nie zawsze może coś zwrócić, użyj nullable-typu jako wartości zwracanej:
// Metoda szuka użytkownika po loginie i zwraca jego wiek, jeśli znajdzie, inaczej null
int? FindUserAge(string login)
{
// ... tutaj logika szukania
return null; // jeśli nie znaleziono
}
Nullable-parametry
Możesz przyjmować nullable-wartości w parametrach, żeby jasno pokazać: “może być, a może nie być”.
void PrintTemperature(int? temp)
{
if (temp == null)
Console.WriteLine("Temperatura nieznana");
else
Console.WriteLine($"Temperatura: {temp} stopni");
}
GO TO FULL VERSION