Denne artikel forudsætter kendskab til generics, som er en del af typesystemet i version 2 af .NET frameworket. Kender du ikke allerede til generics, kan du læse mere om, hvad de er, og hvordan de fungerer, i artiklen Generics.
Om hvordan man putter null, hvor null normalt ikke vil puttes
En af de vigtige forskelle på value- og referencetyper er, at valuetyper i modsætning til referencetyper ikke kan tildeles null (C#) / Nothing (VB) som værdi. Så kunne tænke sig, at sætte en integers værdi til at være udefineret ved brug af null/Nothing, kan det ikke umiddelbart lade sig gøre – i hvert fald ikke i version 1.1 af .NET frameworket. Men det kan det til gengæld – om end indirekte - i version 2 af .NET frameworket ved hjælp af Nullable datatypen. System.Nullable-strukturen er en generisk datatype, der blandt andet gør det muligt at hægte en boolean sammen med selve ens data således, at der holdes rede på, om man repræsenterer en simpel værdi eller en null-/Nothing-værdi. Eftersom Nullable-strukturen er en generisk datatype får vi naturligvis alle de kendte fordele ved .NETs generics med typestærk opførsel såvel som god performance, lavt ressourceforbrug og så videre.
Den generiske System.Nullable struktur
Den generiske Nullable datatype er en struct (C#) / Structure (VB) defineret som følger:
Public Structure Nullable(Of T As Structure)
Private value As T
Private hasValue As Boolean
Public Sub New (value As T)
Public Function Equals(ByVal other As Nullable(Of T)) As Boolean
Public Function GetValueOrDefault (defaultValue As T) As T
Public ReadOnly Property Value() As T
Public ReadOnly Property HasValue() As Boolean
End Structure
public struct Nullable<T> where T: struct
{
private T value;
private bool hasValue;
public Nullable (T value)
public bool Equals(Nullable<T> other)
public T GetValueOrDefault (T defaultValue)
public T Value { get {…} }
public bool HasValue { get {…} }
}
Jeg har udeladt en række af de i denne sammenhæng knap så vigtige metoder og properties for overskuelighedens skyld. Som det ses ud fra constrainten (betingelsen der i det ovenstående er koblet på erklæringen af T – se igen artiklen Generics) kan alle value-typer gøres nullable i modsætning til referencetyper, som ikke kan gøres nullable - men det er der på den anden side heller ikke så megen grund til, eftersom referencetyper allerede er nullable af natur. På den mest grundlæggende form kan man initialisere en nullable variabel med for eksempel følgende kode:
Dim x As New System.Nullable(Of Integer)(17)
System.Nullable<int> x = new System.Nullable<int>(17);
Koden følger de almindelige regler for brug af generiske datatyper, og konstruktøren (Nullable (C#) / New (VB)) gør, at man kan initialisere ens nullable variable med en given værdi. Da den generiske Nullable datatype er en struct/Structure, behøver man dog ikke foretage en eksplicit instansiering; man kan efter en simpel variabel-erklæring vælge blot at benytte variablen direkte - uden instansiering - på følgende manér:
Dim y As System.Nullable(Of Integer) = 17
System.Nullable<int> y = 17;
Hvis man slet ikke angiver en værdi, antager variablen defaultværdien null/Nothing:
Dim z As System.Nullable(Of Integer)
System.Nullable<int> z;
Den implicitte typekonverteringsfunktion er defineret således, at laver man eksempelvis følgende værdi-tildeling af en simpel integer-værdi til den ”nulbare” ;^) (”nullable” på engelsk) integer-variabel x, som er defineret ovenfor:
x = 42
x = 42;
eksekveres ikke overraskende følgende kode:
x = New System.Nullable(Of Integer)(42)
x = new System.Nullable<int>(42);
Så når man tildeler en nullable variable en simpel værdi, behøver man altså ikke gøre noget særligt i forhold til tildelinger af værdier til simple variable. Hvis man går den anden vej (altså tildeler en simpel variabel en nullable værdi) skal man som altid i .NET i forbindelse med begrænsende (”narrowing” på engelsk) typekonverteringer foretage en eksplicit typekonvertering som i dette eksempel:
Dim z As System.Nullable(Of Integer) = 17
Dim i As Integer = CInt(z)
System.Nullable<int> z = 17;
int i = (int)z;
Som altid i forbindelse med begrænsende typekonverteringer er der naturligvis en risiko for, at typekonverteringen kan gå galt og resultere i en runtime fejl – i dette tilfælde hvis z-variablen indeholdt en null-/Nothing-værdi. Hvis man har tildelt en nullable variabel en simpel værdi kan man aflæse denne simple værdi ved brug af Value-propertyen på variablen. Hvis man kommer til at aflæse denne property på en variabel, der har værdien sat til null/Nothing får man en System.InvalidOperationException med beskeden: "Nullable object must have a value.". Så hvordan finder man egentlig ud af, om den nullable variabel rent faktisk er null/Nothing? Der er flere forskellige måder, hvorpå man kan checke om en nullable variabel indeholder en null-/Nothing-værdi.
En af måderne er ved at benytte Equals-metoden:
Dim isTrue As Boolean = x.Equals(Nothing)
bool isTrue = x.Equals(null);
En anden måde er ved at benytte den boolske HasValue-property:
Dim isAlsoTrue As Boolean = y.HasValue
bool isAlsoTrue = y.HasValue;
Default værdier for nullables
Ofte kan man have behov for at ”konvertere” en null-/Nothing-værdi til en simpel værdi – en slags default-værdi. Koden der foretager en sådan konvertering er i sig selv naturligvis ret simpel at lave, men man kan slippe næsten gratis om ved det ved blot at kalde GetValueOrDefault-metoden:
Dim x As System.Nullable(Of Integer) = Nothing
Dim y As System.Nullable(Of Integer) = 42
Dim res As Integer
res = x.GetValueOrDefault(13)
res = y.GetValueOrDefault(17)
System.Nullable<int> x = null;
System.Nullable<int> y = 42;
int res;
res = x.GetValueOrDefault(13);
res = y.GetValueOrDefault(17);
Om det overhovedet giver mening at benytte GetValueOrDefault-metoden afhænger af den enkelte situation. En given default-værdi kan i nogle situationer være så naturlig, at man slet ikke har brug for at benytte nullables - det kunne eksempelvis dreje sig om en sum, hvor 0 (tallet nul) sikkert ofte vil kunne være en helt naturlig default-værdi. Ved datoer kan jeg til gengæld have ret svært ved at bestemme mig for en passende universel default-værdi, om end dags dato kan finde anvendelse i konkrete situationer. Nullable datatyper er netop skabt til situationer, hvor der ikke findes en naturlig universel default-værdi.
Nullable datatyper i C#
Ovenstående eksempler baserer sig alle sammen på den sædvanlige måde at arbejde med generiske typer på. C# har dog også en alternativ syntaks baseret på, at T? svarer til System.Nullable<T>. Nedenstående tabel giver nogle basale eksempler på nullable integers:
Alternativ C# syntaks | Svarende til |
int? z1; | System.Nullable<int> z; |
int? x1 = new int?(17); | System.Nullable<int> x = new System.Nullable<int>(17); |
int? y1 = 17; | System.Nullable<int> y = 17; |
y = x ?? 13; | y = x.GetValueOrDefault(13); |
int? z = (int?) new double?(17.42); | System.Nullable<int> z = (System.Nullable<int>) new System.Nullable<double>(17.42); |
Som det ses er denne alternative C#-syntaks en hel del mere kompakt end den til gengæld nok noget mere eksplicitte syntaks baseret på den sædvanlige notation for generics, men der er altså kun tale om såkaldt syntaktisk sukker uden semantiske - eller eksekveringsmæssige om man vil - implikationer for applikationen. Udover den generiske System.Nullable-struktur (System.Nullable<T> / System.Nullable(Of T)) findes der også en helt almindelig statisk utility-klasse ved navn System.Nullable. Udover statiske funktioner der har pendanter i instans-metoder på den generiske Nullable datatype, indeholder den statiske Nullable-klasse endvidere en række unikke funktioner: GetUnderlyingType, FromObject, ToObject, Wrap og Unwrap, som kan bruges til transformation mellem nullable og simple værdier. I forbindelse med nullable datatyper taler man om, at man i C# har forfremmet (”liftet” på engelsk) de sædvanlige operatorer og typekonverteringer på de simple datatyper til også at gælde for de generiske typer . De ”liftede” operatorer betyder eksempelvis, at fordi +-operatoren er defineret på integers, så er +-operatoren dermed også automatisk defineret på System.Nullable. De ”liftede” typekonverteringer betyder, at kan en type T konverteres til typen S, så kan typen System.Nullable<T> automatisk konverteres til System.Nullable<S>, så System.Nullable<int> kan altså eksempelvist implicit konverteres til System.Nullable<long>. En begrænsning i den generelle ”lifting” af operatorerne er dog sammenligningsoperatorer, som uanset om operanderne indeholder null aldrig resulterer i en System.Nullable, men altid vil give en almindelig boolsk værdi.
null, zip, zero, zilch, nada, the IQ of a rock, absolutely Nothing
I sig selv er nullable datatyper ikke nogen revolution – også i version 1.1 af .NET frameworket kan man ret simpelt selv implementere en lignende funktionalitet – idet man jo i dets essens blot skal have hægtet en HasValue-boolean på de simple grund-data. Resten af den basale funktionalitet kan et langt stykke hen af vejen opnåes ved brug af almindelig indpakning og utility-klasser. Men nullable datatyper er et glimrende eksempel på, hvordan generics kan gøre ens kode simplere samtidig med, at man på grund af generiske datatypers generelt typestærke tilgang får en god programmerings-understøttelse. Har man ikke brug for nullable-egenskaben ved en nullable datatype, bør man som udgangspunkt ikke anvende dem. Dels er der sikkert nogen, der synes, at nullable datatyper gør koden en anelse grimmere ;^) – eller i det mindste mere kompliceret om man vil – end, hvis man blot bruger de simple datatyper; dels er der ikke overraskende et vist performance overhead ved at bruge nullable datatyper frem for de simple datatyper. Tjah hvem ved - måske vil man slet ikke tillade, at ens valuetype variable kan antage null/Nothing, og så kan man jo helt slippe for at tænke mere over emnet ;^)