Gå til indholdet

Indkapsling

Indkapsling er et vigtigt begreb i objektorienteret programmering. Det giver mulighed for at skjule både data (felter) og funktionalitet (metoder) for dem, der benytter typerne til at skabe objekter.

Information til undervisere

Et super vigtigt område for kursisterne at forstå - og det er ikke altid lige nemt. Det er vigtigt at kursisterne forstår at offentlige felter duer ikke fordi man ikke kan styre tilgang og aflæsning. Yderligere skal egenskaber forklares grundigt - mange komme fra sprog hvor get/set metoder er helt normale, og i C# er der jo også get/set metoder. De er bare gemt væk bag autogeneret kode.

I C# er indkapsling så simpelt, at det blot handler om at skifte synlighed på medlemmer. Se følgende klasse:

internal class Terning
{
    public int Værdi;

    public void Ryst()
    {
      // kode
    }
}

Her kan både feltet og metoden ses og benyttes udefra fordi med-lemmer er offentlige (public):

Terning t1 = new Terning();
t1.Værdi = 1;
t1.Ryst();

Men hvis medlemmernes synlighed rettes til privat (private):

internal class Terning
{
    private int Værdi;

    private void Ryst()
    {
      // kode
    }
}

kan hverken feltet Værdi eller metoden Ryst ses udefra, og et eventuelt forsøg på tilgang vil resultere i en kompileringsfejl. Private medlemmer kan udelukkende tilgås fra medlemmer i typen selv.

Umiddelbart kan du måske ikke se fordelen ved at skjule medlemmer, men du skal forstå det således, at du som udvikler af typen bestemmer, hvordan medlemmer skal tilgås ved at skabe en offentlig grænseflade. Hvad der så sker bag facaden er underordnet – det behøver brugeren af typen ikke at forstå.

Indkapsling af felter

Indkapsling bruges især til at styre tilgangen til felter, så du skriver kode relateret til validering, sikkerhed, log og så videre. I mange programmeringssprog benyttes såkaldte get- og set-metoder til at skabe tilgang til et privat felt. Get-metoden aflæser og Set-metoden tildeler. Du vil typisk lade C# kompileren autogenere get- og set-metoderne, men det er vigtigt, du lige har set og forstået teknikken. Hvis du skulle kode det manuelt, kunne det se således ud:

public class Terning
{
    private int værdi;

    public void SetVærdi(int value)
    {
        this.værdi = value;
    }

    public int GetVærdi()
    {
        return this.værdi;
    }
}

Bemærk, at feltet nu er stavet med lille v. Det har ingen praktisk betydning for kompileren, men C# udviklere er vant til denne navn-givningsstandard. Nu er det private felt beskyttet (privat) og tilgang kan udelukkende ske gennem metoderne:

Terning t = new Terning();
t.SetVærdi(1);
Console.WriteLine(t.GetVærdi());    // 1

I metoderne kan du naturligvis placere de instruktioner, du ønsker afviklet, når der tildeles og aflæses – nu er du som udvikler af terningen i kontrol.

Her er eksempelvis koden, der sikrer, at terningen ikke kan få en forkert værdi, og at man kun kan benytte terningen i en weekend:

public class Terning
{
    private int værdi;

    public void SetVærdi(int value)
    {
        if (!this.ErWeekend())
            throw new Exception("Terning må kun bruges i weekenden");
        if (value < 1 || value > 6)
            throw new Exception("Forkert værdi");
        this.værdi = value;
    }

    public int GetVærdi()
    {
        if (!this.ErWeekend())
            throw new Exception("Terning må kun bruges i weekenden");
        return this.værdi;
    }

    private bool ErWeekend()
    {
        DayOfWeek dag = DateTime.Now.DayOfWeek;
        switch (dag)
        {
            case DayOfWeek.Sunday:
            case DayOfWeek.Saturday:
                return true;
            default:
                return false;
        }
    }
}

Bemærk, at metoden ErWeekend også er privat og kun lever i klassen Terning. Den er dermed indkapslet og kan ikke tilgås udefra. Du kan også skabe private felter, som ikke har en get/set metode, og dermed holdes helt internt i klassen.

Her er en lidt mere komplet terning:

public class Terning
{
    private int værdi;
    private System.Random rnd = new System.Random();
    private string[] fejlTekst = {
        "Forkert værdi",
        "Terning må kun bruges i weekenden"
    };

    public void SetVærdi(int value)
    {
        if (!this.ErWeekend())
            throw new Exception(this.fejlTekst[1]);
        if (value < 1 || value > 6)
            throw new Exception(this.fejlTekst[0]);
        this.værdi = value;
    }

    public int GetVærdi()
    {
        if (!this.ErWeekend())
            throw new Exception(this.fejlTekst[1]);
        return this.værdi;
    }

    public void Ryst() {
        this.værdi = this.rnd.Next(1, 7);
    }

    private bool ErWeekend()
    {
        DayOfWeek dag = DateTime.Now.DayOfWeek;
        switch (dag)
        {
            case DayOfWeek.Sunday:
            case DayOfWeek.Saturday:
                return true;
            default:
                return false;
        }
    }
}

Nu er både fejltekster (gentaget kode er roden til alt ondt) og felter, der refererer til en instans af System.Random, holdt privat.

De offentlige medlemmer er de medlemmer, som brugeren af Terning har adgang til. De offentlige og private medlemmer er de medlemmer, som udvikleren af Terning har adgang til.

Egenskaber

Du kan vælge, om du vil benytte get- og set-metoder til at skabe tilgang til et privat felt, men C# stiller en anden medlemstype til rådighed, som du bør benytte i stedet. Det kaldes en egenskab (eller på engelsk - property).

Det giver nogle fordele frem for at kode dine egne get/set metoder. For det første er det nemmere og hurtigere at skrive koden i klassen, for det andet er syntaksen bedre for dem, der benytter klassen, og sluttelig er det ikke bare almindelige metoder, men en speciel medlemstype. Det giver en del muligheder for at benytte klassen til eksempelvis automatisk databinding i brugerflade runtimes (Windows Forms, Windows Presentation Foundation eller Xamarin Mobile), serialisering og deserialisering (eksempelvis fra et JSON format til objekt) eller automatisk kodegenerering i Visual Studio (ASP.NET MVC bruger det eksempelvis meget).

Så du bør benytte egenskaber i stedet for at skabe dine egne get- og set-metoder – for det er jo bare metoder lige som alt andet. I virkeligheden bliver get- og set-metoder autogenereret af kompileren bag om ryggen på os, men det er en helt anden historie.

Warning

Du bør aldrig bruge offentlige felter men altid gøre felter private og “pakke dem ind” i egenskaben fra starten. En senere ændring kan ses som en “breaking change”. Der er ikke nogen direkte ændring i syntaks men der vil opstå problemer med binær kompatibilitet, serialisering, reflektion, kodegenerering med videre. Yderligere vil egenskaber på et senere tidspunkt gøre det muligt at tilføje kode til validering og lignende.

ALDRIG offentlig felter - ALTID private felter med egenskaber.

Komplette egenskaber

Hvis du først har forstået årsagen til brugen af get- og set-metoder, er syntaksen bag egenskaber meget simpel. Den ser således ud:

[datatype] [navn]
{
    get {
   return [værdi]
 }
 set {
      // value er implicit
 }
}

Som du kan se, går get og set notationen igen fra almindelige metoder, men er noget hurtigere at kode. Hvis klassen terning skal benytte en egenskab til værdi, ser det således ud:

public class Terning
{
    private int værdi;

    public int Værdi
    {
        get { return værdi; }
        set { værdi = value; }
    }
}

Tip

Du kan tilføje en egenskab med en snippet i Visual Studio og i Visual Studio Code. Brug ”propfull” og to gange tabulering.

Bemærk, at i set-delen af egenskaben kan variablen value benyttes. Den bliver automatisk tildelt en værdi af runtime og behøver dermed ikke erklæres.

Den nye egenskab kan benyttes som følger:

Terning t = new Terning();
t.Værdi = 4;
Console.WriteLine(t.Værdi);

Bemærk, at syntaksen ved brug nu er meget pænere – der er ikke længere tale om metodekald. Når der bliver tildelt en værdi, bliver set-delen kaldt, og når der bliver aflæst, bliver get-delen kaldt.

Ligesom ved almindelige metoder kan man naturligvis tilføje den kode, man ønsker relateret til validering, sikkerhed eller lignende:

public class Terning
{
    private int værdi;


    public int Værdi
    {
        get
        {
            if (!this.ErWeekend())
                throw new 
                   Exception("Terning må kun bruges i weekenden");

            return this.værdi;
        }
        set
        {
            if (value < 1 || value > 6)
                throw new Exception("Forkert værdi");

            this.værdi = value;
        }
    }

    private bool ErWeekend()
    {
        DayOfWeek dag = DateTime.Now.DayOfWeek;
        switch (dag)
        {
            case DayOfWeek.Sunday:
            case DayOfWeek.Saturday:
                return true;
            default:
                return false;
        }
    }
}

Du kan vælge, hvilken synlighed get- eller set-blokken skal have – eller simpelthen fjerne en af de to for at skabe en ren read-only eller write-only egenskab. Her er et eksempel på en egenskab med en offentlig get (som er standard) og en privat get. Det betyder, at set-blokken kun kan tildeles internt i klassen, og dermed er der ikke nogen grund til at tildele en værdi direkte til feltet.

public class Terning
{
    private int værdi;

    public int Værdi
    {
        get
        {
            return this.værdi;
        }

        private set
        {
            this.værdi = value;
        }
    }

    public Terning()
    {
        this.Ryst();
    }

    public void Ryst()
    {
        System.Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

Samme funktionalitet kunne opnås ved at fjerne set-blokken helt, og dermed skabe en read-only egenskab, men det er best practice altid at gå gennem en egenskab. Så kan eventuel kommende kode samles et sted.

Initialisering

Som alternativ til initialisering af data gennem en konstruktør kan man også i C# initialisere egenskaber ved oprettelse ved brug af tuborg-klammer. Se følgende klasse, der for eksemplets skyld blot består af et felt og en tilhørende egenskab:

class Terning
{
    private int værdi;

    public int Værdi
    {
        get { return værdi; }
        set
        {
            if (value < 1 || value > 6)
                throw new Exception("Forkert værdi");
            værdi = value;
        }
    }
}

Da klassen ikke indeholder en brugerdefineret konstruktør, kan værdien tildeles efter oprettelse som følger:

Terning t = new Terning();
t.Værdi = 6;

Men der findes en alternativ syntaks som er lidt hurtigere at skrive:

Terning t = new Terning { Værdi = 6 };

Bemærk, at det ikke længere er nødvendigt med parenteser, og at kompileren godt kan se, at værdien tilhørende instansen t. Hvis der er flere egenskaber, kan navn og værdi blot adskilles med komma.

Da kompileren jo er ligeglad med linjeskift vil du også se denne måde at initialisere objekter:

Terning t = new Terning 
{ 
    Værdi = 6 
};

Det kan gøre det lidt nemmere at læse, hvis der er mange egenskaber.

Init-egenskab

Som alternativ til en set-egenskab kan du også vælge at benytte en init-egenskab:

class Terning
{
    private int værdi;

    public int Værdi
    {
        get { return værdi; }
        init
        {
            if (value < 1 || value > 6)
                throw new Exception("Forkert værdi");
            værdi = value;
        }
    }
}

Det har den fordel at egenskaben nu kun kan tildeles en værdi ved initialisering med tuborgklammer. Efterfølgende kan egenskaben ikke tildeles en værdi:

Terning t = new Terning
{
    Værdi = 6
};
// t.Værdi = 1;    // fejl

Denne init-del kan ses som alternativ til en klasse med en konstruktør og en read-only egenskab:

class Terning
{
    private int værdi;

    public int Værdi
    {
        get { return værdi; } 
    }

    public Terning(int værdi)
    {
        if (værdi < 1 || værdi > 6)
            throw new Exception("Forkert værdi");
        this.værdi = værdi;
    }
}

Nu skal terningen tildeles en værdi i konstruktøren:

Terning t = new Terning(6);

// t.Værdi = 1;    // fejl

Både denne metode, samt brug af init-kodeordet, giver mulighed for at skabe immutable objekter som, når de først er tildelt en værdi, ikke efterfølgende kan ændres.

Info

Hvorfor ikke bare bruge en konstruktor i stedet for en init-egenskab? For det første er der nogle flere muligheder for at skabe objekter med init-egenskaber uden at skulle bruge en konstruktor, men primært handler det om at biblioteker relateret til eksempelvis serialisering ikke kan arbejde med konstuktører med argumenter (brugerdefinerede konstruktører) men derimod godt med init-egenskaber - se følgende:

string json = "{ \"Navn\": \"Test\" }";
Person p = System.Text.Json.JsonSerializer.Deserialize<Person>(json);

class Person
{
    public string Navn { get; init; }
}

Her kan JsonSerializer deserialisere til en init-egenskab, og objektet p er efterfølgende immutabelt(uforanderligt).

Hvorfor ikke bare offentlige felter

Udover muligheden for at kontrollere en set og en get operation er mange frameworks afhængig af brugen af egenskaber i stedet for offentlige felter. Det gælder både automatisk generede brugerflader, serialisering og meget mere. Prøv eksempelvis følgende:

Person1 p1 = new Person1();
p1.Alder = 25;
p1.Navn = "Ole";

Person2 p2 = new Person2();
p2.Alder = 25;
p2.Navn = "Ole";

System.Console.WriteLine(p1.Alder + " " + p1.Navn);
System.Console.WriteLine(p2.Alder + " " + p2.Navn);

string json1 = System.Text.Json.JsonSerializer.Serialize(p1);
string json2 = System.Text.Json.JsonSerializer.Serialize(p2);
Console.WriteLine(json1);   // tom json struktur
Console.WriteLine(json2);   // komplet json struktur


class Person1
{
    // Offentlige felter
    public int Alder;
    public string Navn;
}

class Person2
{
    // Private felter
    private int alder;
    private string navn;


    // Offentlige egenskaper
    public int Alder
    {
        get { return alder; }
        set { alder = value; }
    }

    public string Navn
    {
        get { return navn; }
        set { navn = value; }
    }

}
Læg mærke til, at Serialize-metoder som udgangspunkt forventer egenskaber og ikke felter.

Info

Prøv evt NuGet pakken Dumpify som gør det nemt at udskrive objekter.

Required-kodeordet

En required egenskab i C# bruges til at markere, at en egenskab skal initialiseres, før et objekt er gyldigt. Dette er introduceret i C# 11 og sikrer, at udviklere ikke glemmer at sætte værdier på nødvendige egenskaber.

public class Person
{
    public required string Navn { get; set; }
    public int Alder { get; set; }
}

Når du opretter en instans af klassen, kræver C#-kompilatoren, at Navn bliver initialiseret:

var person = new Person
{
    Navn = "Anders",
    Alder = 30
};

Hvis du forsøger at undlade at initialisere Navn, vil der opstå en kompilationsfejl:

var person = new Person
{
    Alder = 30
}; // Fejl: 'Navn' skal initialiseres.

Hvis der er en konstruktør i klassen, så fungerer required egenskaber stadig, men de skal stadig initialiseres, enten firekte i konstruktøren eller via en objekt-initializer uden for konstruktøren.

Opgaver

Automatiske egenskaber

Der er masser af situationer, hvor du ikke ønsker at tilføje kode, der afvikles ved get- eller set-blokken, og du kan med rette spørge, hvorfor du så ikke bare kan tilføje et offentligt felt. Men for det første bør alle felter være private, og for det andet kan det være, at du i en senere version af applikationer ønsker at tilføje eventuelt validerings- eller sikkerhedskode. Derfor bør du altid benytte en egenskab – også selv den som udgangspunkt er helt tom og blot fører værdier til og fra et felt.

Det kunne kodes således jævnfør forrige afsnit:

public class Terning
{
    private int værdi;

    public int Værdi
    {
        get
        {
            return this.værdi;
        }

        set
        {
            this.værdi = value;
        }
    }
}

Men hvis det blot er den funktionalitet du ønsker, kan du få kompileren til at autogenere hele strukturen for dig. Det hedder en automatisk egenskab og kan kodes således:

public class Terning
{
    public int Værdi { get; set; }
}

Bemærk at tuborgklammer mangler og det er dermed ikke muligt at tilføje kode til get- eller set-blokken. Til gengæld er den superhurtig at kode, og du overholder best practice ved at benytte en egenskab i stedet for et felt.

Kompileren vil sørge for at autogenere både et private felt samt get- og set-metoder, men du kan altid selv på et senere tidspunkt erstatte den automatiske egenskab med en komplet egenskab. Så længe navn og synlighed er ens, vil du ikke ændre på interfacet til klassen.

Du kan stadig rette synligheden på en automatisk egenskab – eksem-pelvis kan egenskaben Værdi have en offentlig get-blok og en privat set-blok:

public class Terning
{
    public int Værdi { get; private set; }

    public Terning()
    {
        this.Ryst();
    }

    public void Ryst()
    {
        System.Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

Nu kan egenskaben Værdi udelukkende aflæses og ikke tildeles en værdi udefra.

Tip

Du kan tilføje en automatisk egenskab med en snippet i Visual Studio – brug ”prop” og to gange tabulering.

Du kan også vælge at benytte init-kodeordet i stedet for set-kodeordet, og som tidligere vist har det den konsekvens, at data kun kan tildeles ved initialisering:

class Terning
{
    public int Værdi { get; init; }
}

Nu skal du tildele en værdi ved oprettelse:

Terning t = new Terning { Værdi = 6 };
// t.Værdi = 1;    // fejl

Det giver især mening i immutable typer.

Opgaver

Records

I nyere C# er det blevet muligt at skabe en klasse med autogenerede egenskaber samt yderligere funktionalitet. Det kaldes en record og det kan du læse mere om her.