W komentarzu do poprzedniego posta Bartosz napisał, że według niego nowo utworzony obiekt z wynullowanymi polami nie zawsze jest błędem i czasami może mieć sens. Nie zgadzam się z tym zupełnie. Powiem więcej: wg mnie, w językach wysokiego poziomu (chodzi mi o takie, gdzie już nie musimy ręcznie alokować i zwalniać pamięci) wartość null nie ma w ogóle racji bytu!
Wartość NULL występuje w językach takich jak C, czy C++ właśnie po to, żeby reprezentować pusty wskaźnik. Natomiast w językach takich, jak C# czy Java nie zarządza się ręcznie pamięcią, a null w zasadzie nie ma żadnego sensownego uzasadnienia. Ktoś może w tym miejscu zaprotestować i powiedzieć, że null ma np. wiele zastosowań przy implementacji różnego rodzaju kolekcji. Odpowiem na to, że kolekcje da się w językach wysokiego poziomu zaimplementować bardziej sensownie. Zaprezentuję to później na przykładzie języka F#.
Zanim to zrobię, pokażę cztery linijki kodu, które zszokują zatwardziałych Jawowców i C-Sharpowców :-) W F# poniższy kod:
type MyClass()=
member x.Foo () = ()
member x.Foo () = ()
let mutable x = new MyClass()
x <- null
...nie skompiluje się! Kompilator powie, że null nie jest wartością klasy MyClass. Analogicznie jest w przypadku każdej innej klasy zdefiniowanej w tym języku: null nie jest poprawną wartością. W F# słowo kluczowe null występuje tylko i wyłącznie po to, żeby móc korzystać ze standardowych bibliotek .NET-owych.
Biblioteki F# zamiast wartości null używają unii dyskryminowanych (ang. discriminated union). Najprostszą z nich jest option, który jest zdefiniowany tak:
type 'a option =
None
| Some of 'a
None
| Some of 'a
Czyli albo nie mamy wartości (None) albo ją mamy (Some). Option może być użyty w funkcji w następujący sposób:
let makeList opt =
match opt with
| None -> []
| Some x -> [x]
match opt with
| None -> []
| Some x -> [x]
Dla niewtajemniczonych: powyższa funkcja zwraca pustą listę dla braku wartości i jednoelementową listę, gdy poda się wartość.
A co z kolekcjami? Otóż jest równie prosto. Na przykład lista łączona, którą w C# zdefiniowalibyśmy tak:
public class LinkedList<T>
{
private T head;
private LinkedList<T> tail;
[..]
}
{
private T head;
private LinkedList<T> tail;
[..]
}
w językach funkcyjnych zdefiniowana byłaby przy użyciu unii dyskryminowanej w taki sposób:
type 'a LinkedList =
Empty
| NonEmpty of 'a * 'a LinkedList
Empty
| NonEmpty of 'a * 'a LinkedList
Według mnie ta druga definicja jest o wiele jaśniejsza. Rekurencyjna definicja listy łączonej jest tu podana wprost - zupełnie jak w definicjach matematycznych. Natomiast w C# (lub w Jawie) trzeba żonglować referencjami i nullami a przez to kod przestaje tu być zrozumiały sam przez się.