Jak CLR\C# ułatwia pracę z NULLABLE?

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ć.

Leave a Reply

Your email address will not be published.