C# i NULL NullReferenceException. Ile razy w swojej karierze widziałeś ten błąd. Wielu programistów i geniuszy architektury programowania od dawna się zastanawia, że może wartość NULL w typach referencyjnych to duży problem.
W C# nie ma wielokrotnego dziedziczenia. Null jest tym wyjątkiem, bo technicznie dziedziczny on po wszystkich wartościach referencyjnych. Brzmi to, jak łamanie jakieś zasady projektowej
Niektórzy mają śmiałość mówić, że jest to problem wart miliardy dolarów
Wyjaśnijmy ich punkt widzenia
Gdzie informacja, jakie wartości mogą wystąpić przy zwrocie metody
Załóżmy, że metoda aplikacji, do której nie masz dostępu, zwraca typ referencyjny np. string.
Teraz pytanie, czy bez testowania, czytania dokumentacji kodu źródłowego masz wiedze czy wartość NULL z tej metody jest wartością legalną.
Gdybyś mógł oznaczyć string w taki sposób "string?". To byś miał świadomość, kiedy NULL wystąpi, a kiedy nie ma racji bytu
Teraz gdy nie masz pewność co otrzymasz
To sprowadza się do kolejnego problemu. Sprawdzania dla każdego typu referencyjnego czy nie ma on przypisanej wartości NULL
var book = this._bookStoreRepository.Get(1234);
if (book != null)
{
// rest of the logic here.
}
else
{
// logic for the null case here.
}
Ta praktyka nazywa się programowanie defensywny ("defensive programming"). Nie ufasz niczemu i by mieć pewność, że przechwycisz każdą możliwość wartości NULL to wstawiasz takie IF-y wszędzie
Wydaje się to głupie
Jednakże w swoje karierze programisty i być może Twojej pamiętasz, jaka to fajna jest zabawa, gdy na produkcji wyskakuje wyjątek NullReferenceException, a ty nie wiesz gdzie
Nagle taki IF by się przydał, bo łatwiej byś znalazł później ten problem w logach
Null kusi
Jako programista Null także Ciebie kusi, by go zwrócić, gdy nie masz pewność co dokładnie, powinna zwrócić Twoja metoda w jakimś dziwnym przypadku.
Zawsze możesz zwrócić NULL i przesłać problem wyżej.
Pytanie tylko, czy ten NULL mówi co się stało innemu programiście
Rozwiązanie C# 6.0
W C# 6.0 pojawił się Elvis operator ? nazywany także Null-conditional
int? length = people?.Length; // null if people is null
Person first = people?[0]; // null if people is null
if (myDelegate?.Invoke(args) ?? false) { … }
Oczywiście to nie rozwiązuje problemu. Ten wynalazek tylko sprawia, że kod jest bardziej czytelny
Twórcy C# stwierdzi, że pora na prawdziwe rozwiązanie, czyli na pozbycie się wszystkich domyślnych NULL-i w typach referencyjnych. Jednak by nie wywalać kodu, który już działa to stworzyli mechanizm znaczników
Rozwiązanie C# 8.0
C# 8 obecnie działa domyślnie dla .NET Standard 2.1 i .NET Core 3.x
W swoim projekcie jednak muszę powiedzieć, że chce skorzystać z opcji ustalania, że każdy typ referencyjny domyślnie nie powinien przechowywać wartości NULL
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>Enable</Nullable>
</PropertyGroup>
</Project>
W praktyce działa to tak
String jest typem referencyjnym, ale wiem, że ten string "Non_nullablereference" zawsze będzie miał jakiś tekst, więc tylko go deklaruje.
Natomiast dla stringu który będzie miał wartości NULL, dodaje do jego typu znak zapytania.
class Program
{
private static string Non_nullablereferencetype = "Cezary";
private static string? nullablereferencetype = null;
static void Main(string[] args)
{
#pragma warning disable CS8625
// Cannot convert null literal to non-nullable reference type.
Non_nullablereferencetype = null;
#pragma warning restore CS8625
// Cannot convert null literal to non-nullable reference type.
nullablereferencetype = "Adah";
}
}
Gdy próbuje przypisać wartość null dla typu, który go ma nie mieć to, dostaje ostrzeżenie od kompilatora.
Prawdopodobnie, gdybym zmienił coś w ustawieniach kompilatora, to te ostrzeżenie zamieniłoby się na błąd uniemożliwiający mi kompilacje aplikacji.
Dodatkowo istnieje jeszcze operator wybaczający NULL. Jest nim wykrzyknik
Alternatywy
Można skorzystać ze wzorca projektowego Option<T>, by mieć spokój z tymi Nullami
https://github.com/louthy/language-ext
Dodatkowo jest projekt na GitHubie który ma metody i klasy generyczne na ten problem
Zamiast wyjątków i null może chcesz zwracać string z błędem, gdy coś pójdzie nie tak z typem "Either<int,string>"