Jeśli miałbym wskazać jedną z najgorszych decyzji w historii programowania, to null byłby na szczycie listy. Tony Hoare, jego twórca, swego czasu przyznał że to błąd warty miliard dolarów, ponieważ null stał się źródłem niezliczonych problemów w systemach na całym świecie. Sam wielokrotnie popełniałem błędy wynikające ze stosowania nulla, na przykład nie zauważając, że jakaś metoda zaczęła w pewnym miejscu zwracać null, co w PHPie jest raczej chlebem powszednim 🙂
Najczęstsze problemy wynikające z null
Poniżej przygotowałem listę najczęściej spotykanych przeze mnie problemów w codziennej pracy.
- Nieoczekiwane wartości – tutaj da się to tylko skwitować w jednym zdaniu. Niech pierwszy rzuci kamieniem kto nie ustawił nulla bo nie było typowania (pomijam tutaj języki ścisłego typowania chociaż pewnie każdy dotknął JSa) 🙂
- Brak wsparcia w typach – w językach bez ścisłego typowania
nullmoże wprowadzać zamieszanie i wymuszać dodatkowe sprawdzanie. - Nieznajomość zasady get, find (ta dobra praktyka nie pojawia się zbyt często a tym bardziej w literaturze ale warto ją znać) – często deweloperzy używają metod w repozytoriach bez świadomości różnic między find a get:
find()– zwracanull, jeśli nie znajdzie obiektu/-ów. Przyjmuje się tak dlatego że jak przetłumaczysz find na polski to oznacza ono znajdź a znajdź dopuszcza wynik braku danych.get()– zawsze zwraca obiekt a jeśli nie ma danych rzuca wyjątek. Analogicznie do słowa powyżej, jak przetłumaczysz get to masz tutaj pobierz i jest to żądanie w którym nie oczekujesz innego efektu jak pobranie.- Niepoprawna implementacja tych metod prowadzi do sytuacji, w których
nullprzechodzi dalej i generuje błędy w najmniej spodziewanych sytuacjach.
- Problemy w Symfony z Doctrine i Symfony/Form – w PHP, a szczególnie w połączeniu Doctrine i Symfony/Form, często dodajemy
nullna polach w encji, co również prowadzi do błędów. W miejscach, gdzie nigdy nie oczekujemynull(zakładam tutaj że w bazie mamy ustawiony not null), nagle może się on pojawić, co może prowadzić do nieoczekiwanych bugów. Wystarczy przecież że ktoś zmieni definicje tabeli bądź gdzieś przepuści taką wartość a przecież do tego wystarczy brak jednego constraint. W nowszych wersjach Symfony problem ten został zlikwidowany, ale w kodzie legacy nadal może się taka sytuacja zdarzyć. - Brak przeciążania metod w PHP – w PHP ale i w wielu innych językach jak Python, JS itp. brakuje możliwości deklaracji tej samej metody z różnymi parametrami (tzw. method overloading), co często prowadzi do sytuacji, w których deweloperzy zamiast lepiej przemyśleć architekturę, stosują
nulljako wartość domyślną dla opcjonalnych argumentów, co może skutkować błędami.
Jak Rust rozwiązuje problem null?
Rust podszedł do tematu w sposób zdecydowanie inny – po prostu nie ma null. Zamiast tego wprowadzono typ Option<T>, który jednoznacznie wymusza na programiście rozważenie przypadku braku wartości.
Option<T> – eliminacja problemu null
W Rust mamy dwa warianty typu Option<T>:
enum Option<T> {
Some(T),
None,
}
Oznacza to, że zmienna może mieć wartość (Some(value)) lub być pusta (None). Jeśli chcemy użyć wartości, musimy ją jawnie obsłużyć, co wymusza na programiście odpowiednie podejście do obsługi błędów:
fn get_user(id: u32) -> Option<User> {
let user = database.find_user(id);
match user {
Some(u) => Some(u),
None => None,
}
}
Dzięki temu wymuszamy, aby każdy, kto używa tej funkcji, jasno określił, co zrobić w przypadku braku użytkownika.
Result<T, E> – obsługa błędów bez wyjątków
Poza Option<T>, Rust posiada również Result<T, E>, który jest jeszcze potężniejszy, ponieważ pozwala jednoznacznie rozróżnić sytuacje poprawne od błędnych:
enum Result<T, E> {
Ok(T),
Err(E),
}
Dzięki temu funkcje mogą zwracać nie tylko wynik, ale także informację o błędzie:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Can not divide by zero"))
} else {
Ok(a / b)
}
}
A potem używamy match, aby obsłużyć zarówno sukces, jak i błąd:
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(err) => println!("Error: {}", err),
}
Wymuszenie obsługi błędów
W Rust nie możemy „przypadkowo” pominąć sprawdzenia None, co znacząco redukuje błędy:
let user = get_user(10).expect("Użytkownik nie istnieje!");
Albo zapewniamy domyślną wartość:
let user = get_user(10).unwrap_or(default_user());
Podsumowanie
null był historycznym błędem, który kosztował miliardy dolarów i doprowadził do niezliczonych błędów w aplikacjach. Rust w mojej opinii rozwiązują ten problem genialnie dlatego właśnie pojawi się on w tym wpisie. Oczywiście rozwiązanie Rusta można wprowadzać w każdym języku wystarczy tylko ustalona dobra zasada w swoim zespole, obszarze.
Wpis miał na celu pokazać Ci trochę odmienne spojrzenie na nulla i nie oznacza to, że teraz powinieneś rzucić wszystko i całkowicie zrezygnować z null. W wielu przypadkach null może być przydatnym typem do spełnienia zasad DRY, KISS czy SOLID. Problem pojawia się, gdy jest stosowany bezrefleksyjnie – często dodanie null wydaje się szybkim rozwiązaniem problemu, podczas gdy lepsze podejście wymagałoby chwili zastanowienia i innej implementacji. Kluczowe jest więc rozsądne podejście do używania null i świadome projektowanie kodu.
Celowo nie wspomniałem tutaj o bazach danych bo to wgl inny temat na wpis 🙂