Struktura Nullable jest już dobrze znana w świecie .NET. Pozwala na zasymulowanie wartości NULL dla typów prostych (value types). C# posiada jednak wiele ułatwień, które chciałbym opisać w dzisiejszym poście. Prawdopodobnie wiele czytelników korzystało z nich ale nie wiedziała, że to ułatwienie ze strony kompilatora a nie samej struktury Nullable. Zacznijmy od kodu źródłowego Nullable:
[Serializable, StructLayout(LayoutKind.Sequential)] public struct Nullable<T> where T : struct { // These 2 fields represent the state private Boolean hasValue; internal T value; public Nullable(T value) { this.value = value; this.hasValue = true; } public Boolean HasValue { get { return hasValue; } } public T Value { get { if (!hasValue) { throw new InvalidOperationException( "Nullable object must have a value."); } return value; } } public T GetValueOrDefault() { return value; } public T GetValueOrDefault(T defaultValue) { if (!HasValue) return defaultValue; return value; } public override Boolean Equals(Object other) { if (!HasValue) return (other == null); if (other == null) return false; return value.Equals(other); } public override int GetHashCode() { if (!HasValue) return 0; return value.GetHashCode(); } public override string ToString() { if (!HasValue) return ""; return value.ToString(); } public static implicit operator Nullable<T>(T value) { return new Nullable<T>(value); } public static explicit operator T(Nullable<T> value) { return value.Value; } }
Jak widać, struktura sama w sobie nie jest zbyt skomplikowana. Ważnym elementem są operatory konwersji (dwie ostatnie metody), pozwalające na konwersje pomiędzy typem podstawowym a Nullable. Gdyby jednak nie wsparcie C# korzystanie nie byłoby takie łatwe:
1.Zamiast pisać Nullable<int> wystarczy int? – znak zapytania oznacza, że tym ma być owinięty w Nullable:
int? a; Nullable<int> b; // to samo co wyzej
2. Pomimo, że Nullable nie posiada żadnych przeładowanych operatorów, wciąż istnieje możliwość z ich korzystania:
int? a = 5; int? b = 10; int? r = a + b;
C# w zależności od operatora w różny sposób interpretuje wartość NULL. Na przykład jeśli jakaś wartość jest równa NULL wtedy operatory <,>,<=,>= zwracają zawsze false. W przypadku ==,!= zachowują się w identyczny sposób jak dla typów referencyjnych. Podobnych zasad jest sporo i dlatego operowanie na takich typach jest dość skomplikowane – lepiej tego unikać bo łatwo popełnić błąd.
Ponadto operatory generują dużo kodu. Powyższa operacja a+b wygeneruje następujący kod:
Nullable<Int32> nullable1 = a; Nullable<Int32> nullable2 = b; if (!(nullable1.HasValue & nullable2.HasValue)) { return new Nullable<Int32>(); } return new Nullable<Int32>(nullable1.GetValueOrDefault() + nullable2.GetValueOrDefault());
Proszę sobie wyobrazić jak będzie to wyglądało dla skomplikowanych wzorów matematycznych.
Jeśli operujemy na własnym typie np. Matrix i mamy w nim przeładowane różne operatory, C# zachowa się prawidłowo i będzie możliwe wykonanie operacji na macierzach:
Matrix? m1,m2; var r= m1+m2; // wywoła operatory Matrix a nie Nullable<Matrix>, które nie istnieją.
3. Kolejnym ułatwieniem, tym razem ze strony CLR jest boxing. Jak zachowa się poniższy kod?
int? a = null; object boxedA = a;
Mogłoby się wydawać, że zostanie utworzona struktura Nullable<int> i następnie dokonany boxing. Jednak CLR jest na tyle inteligentny, że jeśli Nullable<int> nie ma ustawionej wartości wtedy boxing polega na przypisaniu referencyjnego NULL a nie całego Nullable<int>.
4. Możliwe są następujące sposoby unboxingu:
int val = 5; object boxedInt = val; int? unboxed1 = (int?)boxedInt; int unboxed2 = (int)boxedInt;
Unboxing do Nullable<int> jest możliwy mimo, że tak naprawdę inny typ został zaboksowany (System.Int32). W sposób logiczny również zachowa się unboxing referencyjnego NULL:
object boxedInt = (object)null; int? unboxed1 = (int?)boxedInt; // OK
5. Ciekawostka: co wyświetli poniższy kod?
int? unboxed1 = 5; Console.WriteLine(unboxed1.GetType());
Nullable chce się zachować jak najbardziej transparentnie i wynikiem będzie System.Int32 a nie System.Int32?.
5. Jak widać z kodu źródłowego, Nullable nie implementuje żadnych interfejsów ale mimo tego możliwe jest:
int? val = 5; int result=((IComparable) val).CompareTo(5);
Powyższe obserwacje pokazują, że Nullable zachowuje się prawie jak typ referencyjny. Oczywiście należy pamiętać, że nim nie jest i przypisanie jednego Nullable do drugiego spowoduje skopiowanie wartości. Aczkolwiek, wszelkie konwersje są naturalne i oczekiwane. Należy jednak sobie zdawać sprawę, że jest to wsparcie ze strony C#\CLR i samemu tego kodu nie moglibyśmy po prostu zaimplementować.