Gå til indholdet

Nullables

Værdibaserede variabler med null-værdier

Der findes overordnet to måder at skabe typer, der kan ligge til grund for instanser – klasser og strukturer. Klasser er referencebaserede, og strukturer er værdibaserede. Det har betydning for hvor i hukommelsen værdier placeres.

Information til undervisere

Her gennemgås de to former for nullable - strukturer og klasser. Begge er i virkeligheden ret vigtigt at forstå, men man kan evt starte med at slå Nullable fra i .csproj-filen og så gennemgå strukturerne.

At en variabeltype er referencebaseret betyder, at variabler ikke indeholder værdier, men referencer. Variablen kan dermed tildeles værdien null for at indikere, at den ikke indeholder nogen reference. Det kan værdibaserede variabler ikke, fordi de indeholder konkrete værdier.

Her er et eksempel:

int i = 10; // det er ok – 10 er værdien
// i = null;    // fejl – en int kan ikke få værdien null

En int er det samme som System.Int32, og da det er en struct, kan den udelukkende indeholder værdier.

Men der kan være situationer, hvor det kunne være smart, at en værdibaseret type kan blive tildelt værdien null. Det kan eksempelvis være en bool, der repræsenterer et valg, en bruger har taget på en brugerflade. Værdien kan være true eller false eller null, hvor null indikerer, at brugeren ikke har foretaget et valg.

Det kunne også være et felt i en klasse, som repræsenterer et felt i en database. I de fleste databaser kan man godt kan oprette et tal-felt, der kan tildeles en null-værdi, og det skal nogle gange kunne beskrives i kode.

Derfor har du mulighed at benytte en anden type i C#, som giver mulighed for null-værdi - den generiske System.Nullable-type.

Her er et eksempel, der benytter typen til at gøre en int nullable:

Nullable<int> i;

Nu kan i benyttes, som var det en int:

i = 10;
Console.WriteLine(i);

Men fordi det nu er en Nullable, kan den også tildeles null:

i = null;

og den har fået et par ekstra metoder:

i = 10;
Console.WriteLine(i.HasValue);              // true
Console.WriteLine(i.GetValueOrDefault());   // 10

Egenskaben HasValue fortæller om instansen har en værdi eller er null, og metoden GetValueOrDefault returnerer en værdi, hvis den findes, og eller returnerer en default værdi (som for en int er 0).

For at gøre det nemt at benytte Nullable variabler, kan du også benytte tegnet ? ved erklæring. Således er

Nullable<int> i;

det samme som

int? i;

Det er jo noget nemmere at skrive, men giver præcis den samme type.

Her er et par andre eksempler:

int? a = 10;
bool? b = true;
double? c = 1000.33;
char? d = '*';

a = null;
b = null;
c = null;
d = null;

int? e = LægSammen(10, 10);
Console.WriteLine(e);       // 20
e = LægSammen(null, 10);
Console.WriteLine(e);       // null

int? LægSammen(int? a, int? b)
{
    if (!a.HasValue || !b.HasValue)
        return null;
    else
        return a + b;
}

Info

int? er det samme som System.Nullable

Bemærk, at typen kan benyttes på alle værdibaserede typer, i argumenter, returværdier og også i andre typer:

class Person
{
    public int PersonId { get; set; }
    public string Navn { get; set; }
    public bool? ErDansk { get; set; }
}

I klassen benyttes en nullable bool til at beskrive, om en person er dansk (true), ikke dansk (false) eller det ikke er angivet (null).

Opgaver

Referencebaserede variabler med null-værdier

Info

Se også denne separate forklaring.

Den lidt erfarne C# udvikler vil undre sig lidt over overskriften ”Referencebaserede variabler med null-værdier”, fordi alle referencebaserede variabler (variabler skabt med udgangspunkt i en klasse og ikke en struktur) jo netop kan indeholde en null-værdi – eksempelvis:

class Person
{
    public string Navn { get; set; }
    public string NavnMedStort()
    {
        return Navn.ToUpper();
    }
}

Her er tale om en ganske almindelig klasse med en autogenereret egenskab Navn, og en metode som returnerer navnet med store bogstaver. Den kunne eksempelvis bruges som:

Person p = new Person();
p.Navn = "Mikkel";
Console.WriteLine(p.NavnMedStort()); // MIKKEL

Det fungerer jo fint i dette eksempel, men klassen indeholder en mulig, og faktisk meget udbredt, fejlmulighed. Hvad tror du, der sker, når denne kode afvikles:

Person p = new Person();
Console.WriteLine(p.NavnMedStort()); 

Den fejler med et hult drøn med en NullReferenceException, hvilket i virkeligheden er meget logisk. Metoden NavnMedStort forsøger at afvikle en metode på et objekt, som ikke eksisterer. Navn er jo aldrig tildelt en værdi og har dermed værdien null. Det kan undgås på flere måder – måske ved at ændre metoden så den returnerer null, hvis Navn er lig med null:

class Person
{
    public string Navn { get; set; }
    public string NavnMedStort()
    {
        return Navn?.ToUpper();
    }
}

Eller ved at gøre Set-delen af Navn-egenskaben privat, og værdien tildeles gennem en konstruktør:

class Person
{
    public string Navn { get; private set; }
    public string NavnMedStort()
    {
        return Navn.ToUpper();
    }
    public Person(string navn)
    {
        Navn = navn ?? "";
    }
}

Problemet med mulige null værdier kan løses på mange måder, men det vigtige er, at du er bevidst om eventuelt mulige kommende fejl. I en simpel klasse, er det nemt nok at overskue, men i en kodebase på mange tusinder linjer kan man hurtigt overse et muligt kommende problem. Heldigvis kan man bede kompileren om at være opmærksom på eventuelle null-problemer og oplyse om dette i advarsler eller fejl. Du kan enten dekorere koden med direkte besked til kompileren (# direktiver) som følger:

#nullable enable
    class Person
    {
        public string Navn { get; set; }
        public string NavnMedStort()
        {
            return Navn.ToUpper();
        }
    }
#nullable restore

Eller ved at fortælle kompileren i .csproj-filen (projektfilen), at du ønsker hele projektet kontrolleret for eventuelle fejl:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>
</Project>

Under alle omstændigheder vil kompileren nu fortælle, at der er et problem med Navn.

Advarsel om mulig null-fejl

Advarslen fortæller, at Navn bør tildeles en værdi for at undgå eventuelle fejl – eksempelvis:

class Person
{
    public string Navn { get; set; } = "";
    public string NavnMedStort()
    {
        return Navn.ToUpper();
    }
}

Det løser umiddelbart problemet, men kun indtil du opretter en instans af person og så selv tildeler null til Navn. Men den fanger kompileren også:

Ny mulighed for fejl ved Null værdi

Du kan også vælge at fortælle kompileren, at Navn gerne må få værdien null, og det gøres på samme måde, som når du erklærer en variabel af en nullable struktur:

class Person
{
    public string? Navn { get; set; }
    public string NavnMedStort()
    {
        return Navn.ToUpper();
    }
}

Nu ved kompileren, at Navn kan få en null-værdi, og det skaber en ny advarsel:

Det er ikke nemt at snyde kompileren ved null check

Advarslen fortæller, at ToUpper-metoden kan risikere at fejle, fordi Navn kan have en null-værdi. For at få advarslen væk, er du nødt til at være meget konkret i koden – eksempelvis:

class Person
{
    public string? Navn { get; set; }
    public string NavnMedStort()
    {
        return Navn == null ? "" : Navn.ToUpper();
    }
}

Du kan også fjerne advarslen ved at benytte udråbstegn-operatoren (som meget smukt også hedder ”null-forgiving operator”) som fortæller kompileren, at du tager ansvaret:

class Person
{
    public string? Navn { get; set; }
    public string NavnMedStort()
    {
        return Navn!.ToUpper(); // Eget ansvar!!
    }
}

Under alle omstændigheder tvinges du altså til at tage stilling til eventuelle null-relaterede problemer.

Hvis du ønsker at få deciderede kompileringsfejl (errors) og ikke blot advarsler (warnings), kan du tilføje WarningsAsErrors i .csproj-filen:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <WarningsAsErrors>nullable</WarningsAsErrors>
    </PropertyGroup>
</Project>

Nu kan koden slet ikke kompilere, hvis kompileren opsnuser mulige kommende problemer relateret til null-værdier.