1. Problem NullReferenceException
Prawie każdy początkujący (i nie tylko) programista C# przynajmniej raz spotkał się z tym strasznym komunikatem:
System.NullReferenceException: Object reference not set to an instance of an object.
Klasyka gatunku:
string hello = null;
Console.WriteLine(hello.Length); // BAM! NullReferenceException
Chodzi o to, że próbujesz odwołać się do obiektu, którego nie ma. Czyli zmienna wskazuje na null — puste miejsce zamiast obiektu.
Dlaczego łatwo o taki błąd?
Bo większość typów referencyjnych w C# historycznie mogła przyjmować wartość null. Programiści często zapominali sprawdzić, czy zmienna faktycznie wskazuje na obiekt i w efekcie łapali słynny NullReferenceException.
Gdyby programista dostawał dolara za każdy NullReferenceException, już dawno napisałby własny system operacyjny.
Dlaczego to problem?
Wcześniej nauczyliśmy się, że wartości typu int? albo double? mogą być równe null — na przykład, gdy chcemy jasno zaznaczyć "brak wartości" (np. pole w bazie może być puste).
Ale typy referencyjne (np. string, dowolne klasy, tablice itd.) zawsze mogły być równe null. Tak było od początku C#. To wygodne, ale i niebezpieczne — bo język nie zmuszał nas do myślenia: "A może ta referencja jest pusta?"
2. Ewolucja: Nullable Reference Types (NRT)
5 lat temu pojawiła się nowość, która wywróciła podejście do walki z NullReferenceException do góry nogami — Nullable Reference Types (NRT).
string s = "hello"; // not-nullable reference
string? maybe = null; // nullable reference
Główna idea:
- Wyraźnie oddzielić zmienne-referencje, które na pewno nie mogą być równe null, od tych, które mogą być puste.
- Zacząć pomagać programiście widzieć potencjalne problemy z null już na etapie kompilacji, a nie w runtime (kiedy już za późno).
W nowych wersjach C# zmieniło się podejście do deklarowania zmiennych-referencji: domyślnie nie można przypisać null, jeśli nie pozwolisz na to jawnie.
Tak po prostu — jeden znak ?, a znaczenie zmiennej od razu się zmienia!
3. Włączanie Nullable Reference Types
Domyślnie taki ostry tryb jest wyłączony w większości projektów, żeby nie psuć starego kodu. Ale nowe szablony projektów w Visual Studio, Riderze i .NET CLI już tworzą projekty z włączonym NRT — albo przynajmniej mocno to zalecają.
Żeby sprawdzić, czy NRT są włączone w twoim projekcie, znajdź w pliku .csproj linię:
<Nullable>enable</Nullable>
Jeśli tej linii nie ma, możesz ją dodać ręcznie — to bezpieczne.
Jak to wpływa na kod?
- Jeśli NRT wyłączone (stare zachowanie): string s = null; — żadnych błędów, nikogo to nie obchodzi.
- Jeśli włączone: kompilator zacznie się burzyć, jeśli próbujesz wrzucić null tam, gdzie nie powinno go być.
4. Przykłady z NRT
Prosty przykład
#nullable enable // Ta linia włącza sprawdzanie NRT dla tego pliku
string notNullable = "Cześć";
string notNullable2 = null; // BŁĄD kompilacji!
string? nullableString = null; // Wszystko OK, jawnie pozwoliliśmy na null
W pierwszej linii zadeklarowaliśmy stringa, który zawsze musi wskazywać na prawdziwy obiekt.
W trzeciej — stringa, który może być null.
Sprawdzanie null
void PrintLength(string? s)
{
// Kompilator się burzy: "A co jeśli s == null?"
Console.WriteLine(s.Length);
// A tak - git
if (s != null)
{
Console.WriteLine(s.Length);
}
}
Kompilator teraz pomaga nie zapomnieć o sprawdzeniach!
Ostrzeżenia kompilatora
Jeśli zignorujesz ostrzeżenie i mimo wszystko spróbujesz odwołać się do nullable-zmiennej bez sprawdzenia — dostaniesz nowe (i bardzo przydatne!) ostrzeżenie kompilatora. To nie jest błąd (program się zbuduje), ale żółta "lampka" daje znać: "Jesteś pewien, ziomek?"
Porównanie trybów pracy:
| Typ C# | Czy może być null? | Tryb legacy (przed NRT) | Tryb NRT (#nullable enable) |
|---|---|---|---|
| int | Nie | Nie | Nie |
| int? | Tak | Tak | Tak |
| string | Tak | Nie (zawsze można) | Nie (domyślnie nie można) |
| string? | Tak | Nie | Tak |
5. Porady: jak i po co używać NRT
- Mniej bugów: Mniej niespodziewanych crashy przez null, więcej radości dla devów i userów.
- Czytelność kodu: Od razu widać, co może być puste, a co zawsze musi być ustawione.
- Pomoc od kompilatora: Trzeba przyznać — naprawdę się stara! Ostrzeżenia NRT to cenne info o potencjalnych błędach.
Gdzie to jest niezastąpione
- W dużych projektach, gdzie wiele osób dłubie przy jednym kodzie.
- W API i publicznych bibliotekach — żeby innym jasno pokazać, co mogą, a czego nie.
- Gdzie liczy się niezawodność (np. aplikacje bankowe, systemy medyczne itd.)
6. Typowe błędy i pułapki
- "Zapomniałem dodać ?"
Przypisujesz null zwykłemu stringowi (string s = null;) — a kompilator się burzy, bo domyślnie zwykły string nie powinien być null. - "Przesadziłem z ?"
Robisz wszystkie zmienne string?, byle kompilator się nie czepiał. Chodzi o to, żeby dokładnie oznaczać, gdzie naprawdę dopuszczasz puste wartości. - "Źle zrozumiałem ostrzeżenie"
Ignorujesz ostrzeżenie, a potem łapiesz NullReferenceException tam, gdzie liczyłeś na opiekę kompilatora.
GO TO FULL VERSION