Dine egne typer
Microsoft stiller en masser typer til rådighed, så du har nemt ved at skrive applikationer. Du skal eksempelvis ikke tænke på variabeltyper, fordi Microsoft har skrevet koden bag System.Int32, System.DateTime, System.Boolean og alle de andre typer. Du skal heller ikke tænke på, hvordan du opretter tilfældige tal (System.Random), eller hvordan du tilgår en fil (System.IO.File), for det har Microsoft også tilføjet til runtime.
Information til undervisere
Alle udviklere skal lære objektorienteret (og funktionsorienteret) programmering, og det sværeste er nok at forstå hvad man egentlig kan bruge det til - derfor dette kapitel. Jeg bruger “Terningen” rigtig meget fordi den er så simpel at forstå men det er naturligvis op til dig. Nogen undervisere bruger personer, spillekort, noget fra et spil med videre.
Men det er vigtigt at den studerende ved hvad en klasse er og kender de forskellige medlemstyper i C#.
Syntaks og kodeforståelse er ikke så vigtigt her - det kommer i efterfølgende kapitler.
Skabeloner
Du kan se typer som en skabelon, der indeholder data og funktionalitet, og som kan bruges til at oprette objekter i applikation. Det kan lyde lidt abstrakt, men prøv at forestille dig som en del af C# design teamet hos Microsoft, og I skal tage stilling til hvordan C# udviklere skal kunne arbejde med filer. I et proceduralt sprog ville I måske have skrevet en masse metoder som GetFileLength
, GetFileExtension
, WriteContentToFile
og så videre. Metoderne vil alle tage en filsti som parameter samt andre nødvendige parametre (som f.eks. filindhold). Metoderne vil måske også returnere en værdi eller udføre en handling.
Der er intet galt i den måde at tilbyde generisk funktionalitet på, men det giver lidt udfordringer. Eksempelvis er det svært for udviklere at vide hvor de skal lede efter metoderne, og det bliver kompiceret at sende data om filer rundt i applikationen. Hvis information om en fil bliver splittet op i flere værdier (som f.eks. filsti, filnavn, filstørrelse, filindhold og så videre) kan det blive svært at holde styr på det hele - for slet ikke at tale om at sende disse værdier rundt i applikationen.
Hvis man i stedet havde valgt at benytte objektorienteret programmerig og oprette en skabelon (klasse), der repræsenterer en fil (f.eks. FileInfo
) er det hele mere simpelt. Skabelonen kunne indeholder egenskaber som Name
, Length
, Extension
og metoder som ReadContent
, WriteContent
og Delete
. Det er nemmere at finde metoderne, meget nemmere for udviklingsmiljøer at tilbyde hjælp (den ved jo også hvad en fil
er - det står i skabelonen) og det er nemmere at sende information om en fil rundt i applikationen. Sidstnævnte fordi alle informationer om en fil er samlet i en enkelt enhed, og man blot kan sende en reference (i nogle sprog kaldet en pointer) til filen rundt i applikationen. Det er ikke bare nemt for udviklerne, men der er også en række performancefordele ved at sende referencer rundt i stedet for at kopiere data. Hvis man sender data om en fil rundt i applikationen, vil der hurtigt blive kopieret mange megabytes data, og det kan være en udfordring for applikationens ydeevne. Bliver der sendt en referece til noget i hukommelsen rundt, er det blot en lille værdi, der sendes, og det er hurtigt og nemt.
Info
I den mere avancerede C# vil udvikling af egne strukturer (structs) også kunne bruges - men glem strukturer lige nu og fokuser på klasser.
Egne skabeloner
Men Microsoft ved jo naturligvis ikke hvilke applikationer, du skal udvikle, eller hvordan du vil strukturere koden, så derfor har du mulighed for at skabe dine helt egne skabeloner ved at oprette egne klasser. Så kan du selv skabe en Person
, Faktura
, Terning
, Spiller
, Bil
, Hund
eller hvad du nu har brug for. Du kan også skabe en klasse, der repræsenterer en fil, hvis du ikke synes at FileInfo
er god nok.
Så helt grundlæggende giver udvikling af egne typer mulighed for at definere elementer i din applikation som består af data og funktionalitet samt mere eller mindre lever i sin egen lille verden men kan kommunikere med andre typer.
Se en type som en skabelon du kan skabe instanser af (også kaldt objekter).
De objektorienteret principper
Hvis du kigger ned i teorien omkring objektorienteret programmering vil du typisk finde koncepter såsom arv, polymorfisme, indkapsling og abstraktion. Her er en kort beskrivelse af hvert koncept:
- Abstraktion: Klasser kan bruges til at modellere generelle koncepter og abstraktioner, hvilket gør det lettere at arbejde med komplekse problemer og forbedrer kodeforståelsen.
- Indkapsling: Klasser kan indkapsle deres data og metoder og skjule deres interne tilstand for omverdenen, hvilket sikrer en bedre styring af adgangen til objekter.
- Arv: Klasser kan arve funktionalitet og egenskaber fra andre klasser, hvilket gør det muligt at genbruge og udvide eksisterende kode.
- Polymorfisme: Klasser kan have flere former ved at implementere forskellige interfaces eller ved at nedarve fra forskellige basisklasser. Dette muliggør en mere fleksibel og dynamisk opførsel i applikationer.
Alle koncepter er vigtige i objektorienteret programmering og hjælper med at skabe mere struktureret, genbrugelig og vedligeholdelig kode, og i C# er klasser det primære værktøj til at implementere disse koncepter.
Her er en lidt dybere gennemgang af de forskellige koncepter med lidt kode. Du skal ikke fokusere på selve koden men forsøge at forstå selve konceptet. Senere vil du selv kunne skrive koden.
Abstraktion
Abstraktion handler om at forenkle komplekse systemer ved at fokusere på de væsentligste detaljer og skjule de irrelevante eller komplekse elementer for brugeren. I objektorienteret programmering gør vi dette ved at bruge klasser til at modellere generelle koncepter, hvilket gør det lettere at arbejde med komplekse problemer og forbedrer kodeforståelsen.
En god illustration af abstraktion i C# er System.IO.FileInfo
-klassen, der repræsenterer filer i filsystemet. Brugeren af klassen behøver ikke bekymre sig om, hvordan operativsystemet håndterer filer internt. I stedet præsenteres der en simpel grænseflade, der tillader brugeren at udføre operationer som at få filstørrelse, ændre navn eller slette filer.
FileInfo fileInfo = new FileInfo("example.txt");
Console.WriteLine($"Filnavn: {fileInfo.Name}");
Console.WriteLine($"Filstørrelse: {fileInfo.Length} bytes");
Console.WriteLine($"Sidst ændret: {fileInfo.LastWriteTime}");
Her abstraherer FileInfo
de lavniveau-operationer, der foregår i baggrunden, som systemkald og hukommelsesstyring. Det giver en simpel og nem måde at arbejde med filer på uden at kende alle detaljer om filsystemet.
Et andet eksempel på abstraktion kunne være en klasse, der repræsenterer en terning i Yatzy-spillet. Her skal brugeren blot forholde sig til at “slå” terningen og få en værdi mellem 1 og 6, uden at forstå, hvordan tilfældighedsgeneratoren fungerer bag kulisserne - eller “printe” uden at forstå hvordan det er implementeret:
Terning terning = new Terning();
terning.Ryst();
terning.Print();
class Terning
{
private Random _random = new Random();
public int Værdi { get; private set; }
// Ryst metoden simulerer et terningekast
public void Ryst()
{
Værdi = _random.Next(1, 7); // Sætter værdi til et tal mellem 1 og 6
}
// Print metoden viser terningens aktuelle værdi
public void Print()
{
Console.WriteLine($"Terningen viser: {Værdi}");
}
}
I dette eksempel skjuler Terning
-klassen de interne detaljer ved tilfældighedsgenerering. Brugeren af klassen behøver kun at forholde sig til, at terningen kan rystes og derefter vise en værdi. Alle de komplekse operationer, som f.eks. tilfældighedsgenerering, bliver abstraheret væk.
Abstraktion gør det altså muligt at arbejde med komplekse systemer uden at blive overvældet af detaljer. Man kan fokusere på de essentielle aspekter af et problem og overlade de interne detaljer til de abstrakte klasser.
Indkapsling
Indkapsling handler om at beskytte data ved at skjule objektets interne tilstand og kun tillade adgang eller ændring gennem definerede grænseflader. Dette sikrer, at dataene kun kan påvirkes på kontrollerede måder, hvilket reducerer fejl og gør koden mere robust.
StringBuilder
er en klasse i .NET, der anvender indkapsling til at beskytte de interne strukturer, som bruges til at manipulere en streng. Brugeren interagerer kun med de offentlige metoder og egenskaber for at tilføje, fjerne eller ændre indholdet af en streng, mens alle komplekse detaljer er skjult.
using System.Text;
StringBuilder sb = new StringBuilder();
// Tilføjelse af tekst til StringBuilder
sb.Append("Hej");
sb.Append(" ");
sb.Append("Verden!");
Console.WriteLine(sb.ToString());
Her er det vigtigt at bemærke, at vi som brugere af StringBuilder
ikke behøver at vide, hvordan hukommelse håndteres eller hvordan strengen internt opbygges. Alt det er indkapslet bag Append()
-metoden, og vi har kun adgang til de offentlige metoder, der tillader os at manipulere strengens indhold.
Vi kan også indføre indkapsling i vores tidligere Yatzy-terning-eksempel for at beskytte terningens interne tilstand. For eksempel kan vi gøre terningens værdi privat og kun tilgængelig via en offentlig egenskab, så den kun kan ændres ved at ryste terningen:
public class Terning
{
private Random _random = new Random();
private int _værdi; // Privat værdi, der ikke kan ændres direkte udefra
public int Værdi
{
get { return _værdi; }
}
// Ryst metoden simulerer et terningekast
public void Ryst()
{
_værdi = _random.Next(1, 7); // Sætter værdi til et tal mellem 1 og 6
}
// Print metoden viser terningens aktuelle værdi
public void Print()
{
Console.WriteLine($"Terningen viser: {Værdi}");
}
}
I dette eksempel er værdi
variablen indkapslet, så brugeren ikke kan ændre den direkte. Den kan kun ændres via Ryst()
-metoden, som sikrer, at terningen kun ændrer værdi, når der udføres en korrekt operation (rystning).
Indkapsling hjælper med at beskytte objektets integritet ved at kontrollere, hvordan dets tilstand ændres. Microsofts StringBuilder
-klasse og vores egen Terning
-klasse illustrerer, hvordan indkapsling kan anvendes til at skjule implementeringsdetaljer og sikre, at objekter kun kan manipuleres via definerede metoder.
Nedarvning
Nedarvning gør det muligt at definere en ny klasse, der arver egenskaber og metoder fra en eksisterende klasse. Den nye klasse kan enten genbruge eller tilpasse de arvede egenskaber og metoder. Dette gør det nemt at skabe hierarkier af klasser, der deler fælles funktionalitet, men samtidig tillader specialisering.
Et velkendt eksempel på nedarvning i .NET er System.Exception
, som er en grundlæggende klasse for fejlhåndtering. Når vi laver vores egne undtagelser, kan vi nedarve fra Exception
-klassen for at tilføje specifik funktionalitet eller meddelelsesformater.
public class CustomException : Exception
{
public CustomException(string message) : base(message)
{
}
}
Her arver CustomException
fra Exception
-klassen og tilføjer ingen ny funktionalitet, men vi kunne tilføje flere detaljer, hvis det var nødvendigt.
I Yatzy-spillet kan vi forestille os, at vi har en generel Terning
-klasse, som vi allerede har implementeret. Hvis vi vil skabe en speciel type terning, f.eks. en terning med flere sider, kan vi lave en ny klasse, der nedarver fra Terning
, og tilpasse den til vores behov.
public class Terning
{
protected Random _random = new Random();
protected int _værdi;
public int Værdi
{
get { return _værdi; }
}
public virtual void Ryst()
{
_værdi = _random.Next(1, 7); // Standard terning med 6 sider
}
public void Print()
{
Console.WriteLine($"Terningen viser: {Værdi}");
}
}
public class TolvSidetTerning : Terning
{
public override void Ryst()
{
_værdi = _random.Next(1, 13); // 12-sidet terning
}
}
Her har vi en TolvSidetTerning
-klasse, der nedarver fra Terning
og tilpasser Ryst()
-metoden til at give et tal mellem 1 og 12 i stedet for den sædvanlige 6-sidede terning. Ved at bruge nedarvning kan vi genbruge det meste af funktionaliteten fra Terning
-klassen, som f.eks. Print()
-metoden, og kun ændre det, der er nødvendigt.
Nedarvning tillader os at genbruge eksisterende kode og specialisere den, hvor det er nødvendigt. I eksempler som System.Exception
og vores udvidelse af Terning
-klassen kan vi se, hvordan nedarvning gør koden både mere fleksibel og lettere at vedligeholde.
Info
Som du vil lære senere vil sætningen det som er i mor er også i barnet
være en god huskeregel for nedarvning, og grundlæggende set betyder det at en klasse kan nedarve fra en anden klasse og dermed få alle de egenskaber og metoder som den anden klasse har. Men man vil aldrig kunne fjerne noget fra barnet - kun tilføje eller (nogen gange) ændre.
Polymorfi
Polymorfi er et af de mere komplekse principper i objektorienteret programmering, men når det først er forstået, bliver det et utrolig stærkt værktøj til at skrive fleksibel og vedligeholdelsesvenlig kode. Polymorfi betyder, at objekter kan tage mange former, og i praksis gør det muligt at behandle objekter af forskellige typer ens, så længe de deler en fælles grænseflade eller baseklasse.
Polymorfi giver os mulighed for at skrive kode, der kan arbejde med forskellige objekter på en ensartet måde. Det gør det muligt at bruge en fælles metode på tværs af forskellige objekter, der kan have forskellige implementeringer af den samme metode. Dette gør det muligt at tilføje nye typer objekter uden at ændre den eksisterende kode, hvilket gør systemet meget mere fleksibelt.
En klassisk anvendelse af polymorfi i .NET er brugen af samlinger (collections). For eksempel kan vi arbejde med en række forskellige samlingstyper (lister, køer, stakke osv.), der alle implementerer en fælles grænseflade, som f.eks. IEnumerable
. Dette gør det muligt at iterere over dem ens, selvom deres interne implementeringer er meget forskellige.
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
Her bruges polymorfi til at behandle List<int>
som en IEnumerable<int>
, hvilket betyder, at vi kan iterere over listen uden at bekymre os om den præcise type af samlingen. Dette er et eksempel på subtypet polymorfi.
Vi kan også bruge polymorfi i Yatzy-terningeksemplet, hvor vi har forskellige typer terninger. Vi ønsker at kunne ryste forskellige typer terninger, men vi vil gerne gøre det via en fælles grænseflade, så vi ikke behøver at skrive ny kode for hver enkelt type.
public class Terning
{
protected Random _random = new Random();
protected int _værdi;
public int Værdi
{
get { return _værdi; }
}
public virtual void Ryst()
{
_værdi = _random.Next(1, 7); // Standard terning med 6 sider
}
public void Print()
{
Console.WriteLine($"Terningen viser: {Værdi}");
}
}
public class TolvSidetTerning : Terning
{
public override void Ryst()
{
_værdi = _random.Next(1, 13); // 12-sidet terning
}
}
public class Spil
{
public void RystOgPrint(Terning terning)
{
terning.Ryst();
terning.Print();
}
}
I dette eksempel kan vi bruge polymorfi til at arbejde med både en almindelig terning og en 12-sidet terning på samme måde. Metoden RystOgPrint()
accepterer en Terning
som parameter, men kan ryste og printe resultatet for enhver terningtype, der nedarver fra Terning
-klassen.
Spil spil = new Spil();
Terning standardTerning = new Terning();
TolvSidetTerning tolvSidetTerning = new TolveSidetTerning();
spil.RystOgPrint(standardTerning);
spil.RystOgPrint(tolveSidetTerning);
Her ser vi, at Spil
-klassen kan håndtere begge typer terninger uden at kende deres konkrete implementering. Dette er et eksempel på subtypet polymorfi, hvor den konkrete type af terningen bestemmer, hvilken version af Ryst()
-metoden der kaldes.
Polymorfi gør det muligt at skrive generel kode, der kan arbejde med forskellige objekter på en ensartet måde. Eksempler som IEnumerable
i .NET og vores terningeksempel viser, hvordan polymorfi kan bruges til at håndtere forskellige objekttyper uden at gå på kompromis med fleksibiliteten.
Den helt simple skabelon
Hvis du vil prøve at skabe en helt simpel skabelon der kan holde data og funktionalitet, kan du starte med at oprette en såkaldt record, som er en af kompileren autogenereret klasse. Mere om det senere men det er en nem måde at komme i gang på.
Nu har du en klasse, der kan holde data om en person, og som har en konstruktør, der tager navn og alder som parametre. Du kan oprette en instans af klassen og tilgå dataene på følgende måde:
Det er den mest simple måde at skabe en klasse på, og du kan tilføje flere egenskaber og metoder efter behov. Om ikke andet er det en god måde at komme i gang på.
Brug af klasser
Klasser i C# er grundlæggende byggesten i C# og kan bruges til mange forskellige formål - eksempelvis
Modellering af data
Klasser kan bruges til at modellere data og repræsentere objekter i applikationen. Ved at definere klasser, der indeholder egenskaber og metoder, kan applikationen arbejde med data på en objektorienteret måde og opnå en bedre struktur og organisation af koden. Eksempler på klasser, der modellerer data, inkluderer Person
, Bil
, Terning
og Faktura
og så videre.
Serialisering
Klasser kan serialiseres og deserialiseres til forskellige dataformater, såsom JSON, XML og binært format. Serialisering er en nyttig teknik til at gemme objektets tilstand, overføre data mellem applikationslag eller kommunikere med eksterne systemer. Der er flere biblioteker tilgængelige i .NET, som hjælper med serialisering og deserialisering af objekter, såsom Newtonsoft.Json og System.Text.Json.
Kommunikation mellem applikationslag
Klasser kan bruges til at oprette dataoverførselsobjekter (DTO’er) og modelklasser, som hjælper med at sende data mellem forskellige applikationslag. DTO’er og modelklasser kan opfattes som “containere” for data og bruges til at sikre, at kun relevante data og egenskaber sendes mellem lagene. Dette hjælper med at holde applikationslagene adskilt og letter vedligeholdelsen af applikationen.
Afkobling af kode
Klasser kan bruges til at afkoble forskellige dele af en applikation ved hjælp af designmønstre og arkitektoniske principper. Nogle eksempler på designmønstre, der bruger klasser, inkluderer:
- Observer: Klasser kan bruges til at implementere observer-mønsteret, hvor ét objekt (subject) underretter et sæt af afhængige objekter (observers) om ændringer i dets tilstand. Dette reducerer koblingen mellem objekter og gør det lettere at vedligeholde og udvide applikationen.
- Factory: Klasser kan bruges til at implementere factory-mønsteret, hvor en klasse er ansvarlig for at oprette objekter af en anden klasse. Dette gør det muligt at afkoble objektoprettelse fra den del af koden, der bruger disse objekter.
- Dependency Injection: Klasser kan bruges til at implementere dependency injection, hvor en klasse får sine afhængigheder leveret af en anden klasse eller et framework. Dette reducerer klassens afhængighed af konkrete implementeringer og gør det lettere at teste og vedligeholde koden.
Exception handling
Klasser kan bruges til at definere egendefinerede undtagelser i applikationen, hvilket gør det lettere at identificere og håndtere fejl og undtagelser på en meningsfuld måde. Ved at definere egendefinerede undtagelsesklasser kan applikationen håndtere fejl og undtagelser mere præcist og give mere nyttige fejlbeskeder til brugeren.
Udvidelsesmetoder
Udvidelsesmetoder er en anden måde at tilføje funktionalitet til eksisterende klasser uden at ændre deres kode. Ved at definere en statisk metode i en statisk klasse med det første parameter, der angiver den klasse, metoden skal udvide, kan man tilføje nye metoder til en eksisterende klasse.
Multitrådning og asynkron programmering
Klasser kan bruges til at håndtere multitrådning og asynkron programmering i C#. Ved at definere metoder og egenskaber, der understøtter parallelle og asynkrone operationer, kan applikationen drage fordel af moderne hardware og levere en bedre ydelse og brugeroplevelse. Eksempler på klasser i .NET, der understøtter multitrådning og asynkronitet, inkluderer Task
, Task<TResult>
og SemaphoreSlim
.
Middleware og pipelines
Klasser kan bruges til at opbygge middleware og pipelines i applikationer, især webapplikationer. Middleware er komponenter, der eksempelvis håndterer HTTP-anmodninger og -svar i en pipeline. Ved at oprette klasser, der implementerer et bestemt interface eller følger et bestemt mønster, kan applikationen opbygge en fleksibel og skalerbar pipeline til behandling af HTTP-anmodninger.
Dataadgang og ORMs
Klasser kan bruges til at repræsentere data og arbejde med objekt-relationelle mapper (ORMs) som Entity Framework. Ved at definere klasser, der repræsenterer databasetabeller og deres relationer, kan applikationen arbejde med data på en objektorienteret måde og minimere afhængigheden af SQL og databasen.
Refleksion og metaprogrammering
Klasser kan bruges til at arbejde med refleksion og metaprogrammering i C#. Refleksion giver mulighed for at inspicere og interagere med kode ved kørsel, mens metaprogrammering gør det muligt at generere og manipulere kode ved kørsel. Klasser som Type
, MethodInfo
og Expression
i .NET-rammen muliggør refleksion og metaprogrammering i C#-applikationer.
Spiludvikling og grafik
Klasser kan bruges til at oprette spil og grafiske applikationer ved hjælp af rammer som Unity og MonoGame. Ved at definere klasser, der repræsenterer spilobjekter, scener, kameraer og andre elementer, kan applikationen oprette komplekse og interaktive spil og grafiske applikationer.
Eksempel på grundlæggende OOP
Lad os antage, at du skal skabe en Yatzy-applikation.
I de traditionelle programmeringsparadigmer som iterativ og procedural programmering skal du fokusere på flowet i applikationen. Når spillet afvikles, skal du løbende opbevare data i variabler og arrays med spillernes navne, deres point, hvis tur det er og værdien af et slag med terningerne. Og du vil sikkert skabe metoder som SpillerMedFlestPoint, ErDerFuldtHus, ErderYatzy, ErSpilletSlut og så videre for at gøre koden så nem at udvikle og vedligeholde som muligt. Du vil uden de store problemer, kunne skabe et Yatzy-spil på den måde. Sådan er millioner af applikationer skrevet siden 1950’erne og bliver det uden tvivl også i skrivende stund.
I objektorienteret programmering, eksempelvis ved hjælp af et sprog som C#, kan du vælge (du behøver ikke – det er et valg fra din side) at benytte objektorienterede principper for at gøre koden nemmere at skrive, forstå, vedligeholde og teste – både for dig selv og andre. Du kan vælge at finde elementer (kaldes også entiteter) i applikationen, som du kan repræsentere med en definition. Denne definition kaldes også en skabelon, og i C# kan du vælge mellem en klasse (class), post (record) eller en struktur (struct). I grundlæggende C# kan du som nævnt se bort fra at oprette strukturer, og poster bliver gennemgået senere. Lige nu skal du blot fokusere på klasser.
En klasse er en skabelon, der ligger til grund for oprettelse af objekter, som lever i din applikation. Objekter bliver også kaldt for instanser, men der er tale om det samme. Disse objekter bliver oprettet i din kode og eksisterer i hukommelsen i det tidsrum, du ønsker. De mest simple klasser består blot af en enkelt variabel, der repræsenterer din entitet, men du kan tilføje så mange variabler, du ønsker. Du kan dermed se klasser som relaterede variabler, der repræsenterer et eller andet.
Hvis du vil skabe Yatzy-spil i objektorienteret kode, skal du starte med at lede efter entiteter, og du vil hurtigt kunne identificere entiteter som en terning, et bæger med terninger, en pointtavle, en spiller, selve spillet og så videre. Du skal prøve at finde så atomare entiteter som muligt forstået således, at en entitet ikke logisk kan opdeles yderligere. En terning er eksempelvis atomar fordi det næppe vil give mening at tænke på en terningside som en enkeltstående entitet, og en terning dermed består af en samling af terningsider. Men det er et arkitektonisk valg.
Du kunne også vælge at se en spilleplade som en entitet med et navn og en samling af point, men en mere erfaren udvikler vil måske se entiteter som spiller, point og en spilleplade som består af en spiller og en samling point. Det er netop det, der er svært i objektorienteret programmering – at identificere identiteter. Det kræver erfaring og øvelse.
Hvis du eksempelvis skulle skabe et ERP (Enterprise Resource Planning) system vil du kunne identificere entiteter som person, selskab, kunde, vare, lager, faktura, fakturalinje og så videre.
Tit er de entiteter, du finder, noget du kan relatere til fysiske ting – en person, en terning, en vare og så videre, men det kan også være abstrakte ting som eksempelvis noget, der repræsenterer flowet i et Yatzy-spil, en risikovurdering på en kunde eller en algoritme til at beregne effektiv rente på en obligation.
Mange projekter starter med en brainstorm over entiteter, og et stort whiteboard, cola og pizza plejer at være en god måde at komme i gang på. Der findes også flere strukturerede måder at skabe sig et overblik over en applikation, og UML (Unified Modeling Language) er en af de mest kendte standarder. Nogle udviklere kan også godt lide blot at definere entiteter i koden efterhånden som de dukker op. Det bruges eksempelvis i moderne agil udvikling.
Terningen
Jeg vil bede dig om at gøre en ting, som du vil syntes er underligt – måske endda tåbeligt. Men stol på mine mindst 15 års erfaring i undervisning i programmering og bare gør, hvad jeg beder om.
Du skal i en skuffe et sted finde en almindelig terning! Altså en terning fra et Yatzy- eller et brætspil, med seks sider og numrene 1-6. En ganske almindelig terning – størrelse og farve er underordnet, men hvis du kan vælge så tag den terning, du umiddelbart bedst kan lide.
Det næste stykke tid vil jeg gerne have, at du har terningen i nærheden af dig – i hånden, i lommen, i tasken eller stående på bordet ved siden af dit tastatur. Terningen skal på en eller anden måde være i din bevidsthed et stykke tid, og du skal helst sørge for at have fingrene i den et par gange om dagen. Terningen skal minde dig om, hvordan objektorienteret kode fungerer og holde disse tanker friske i din hukommelse.
Vi skal bruge terningen til flere ting, men til at starte med skal vi se på, hvordan du kan skabe en klasse (skabelon), der repræsenterer en terning. Denne klasse kan ligge til grund for objekter, og allerede nu er du i gang med at tænke objektorienteret, fordi det som udgangspunkt er ligegyldigt i hvilke terningespil, du skal bruge klassen. En terning er en terning, og du kan derfor genbruge klassen i mange forskellige applikationer.
Medlemstyper
I en klasse kan du skabe forskellige typer af funktionalitet - det kaldes medlemstyper, når det er relateret til strukturer og klasser. I C# har du mulighed for felter, der repræsenterer klassens data, egenskaber som typisk bruges til at beskytte felterne imod forkert tildeling og aflæsning, metoder der kan bruges mod klassens data, og hændelser som giver mulighed for at få afviklet kode, når en speciel hændelse sker. Sluttelig findes der kode, som afvikles, når der oprettes et objekt (kaldes en konstruktør), og kode som afvikles, når et objekt bliver fjernet fra hukommelsen af runtime (kaldes en destruktør). Du bestemmer helt selv, hvor mange felter, egenskaber, metoder, hændelser og konstruktører du ønsker, men der kan kun være én destruktør.
For god ordens skyld er her navnene på alle de mulige medlemstyper på både dansk og engelsk:
Medlemstype på dansk | Medlemstype på engelsk |
---|---|
Felt/Felter | Field/Fields |
Egenskaber | Property/Properties |
Metode/Metoder | Method/Methods |
Hændelse/Hændelser | Event/Events |
Konstruktør | Constructor |
Destruktør | Destructor |
Synlighed
Alle medlemstyper kan have forskellig synlighed. For nemmest at kunne forstå dette begreb er det vigtigt, at du kan se en klasse fra to udvikleres synspunkt – den udvikler, der skriver koden til klassen, og den udvikler, som benytter klassen. Det kan naturligvis godt være en og samme person, men det er nemmere at forstå, hvis de to personer er adskilt.
En medlemstype kan i helt grundlæggende C# være enten privat eller offentlig. En privat medlemstype kan udelukkende benyttes af andre medlemmer i klassen selv, og kan slet ikke ses uden for klassen. Så den er altså tilgængelig for udvikleren, som skriver koden til klassen, men ikke for udvikleren, der benytter klassen. Derfor siger man, at medlemstypen er privat og ikke synlig udefra.
En offentlig medlemstype kan benyttes af alle - både af medlemmer i klassen selv og af kode, der benytter klassen. Eller sagt på en anden måde. En offentlig medlemstype kan både benyttes af den udvikler, som skriver koden til klassen, og af den udvikler, som benytter klassen til at skabe objekter.
Du kan altså godt skabe en klasse med mange medlemstyper, hvoraf nogle er private og andre er offentlige. På den måde kan du gemme nogle medlemmer væk som er ligegyldige for brugeren af klassen.
Da du sikkert støder på synlighedsbegrebet i dokumentation og artikler er her den engelske oversættelse.
Synlighed på dansk | Synlighed på engelsk |
---|---|
privat | private |
offentlig | public |
Alle disse forskellige medlemstyper og deres synlighed kan virke lidt abstrakt, men hvis du tænker på din terning, kan vi gøre det mere håndgribeligt.
Felter
Hvis du skal skabe noget, som skal repræsentere en terning, skal du i hvert fald have ét felt (variabel) til at opbevare værdien af terningen. Den kan jo have værdien 1-6, så du skal finde en datatype som passer. Det kunne være en af de mange heltalstyper som byte eller int, men det er helt op til dig. Måske en streng eller en enumeration kunne bruges?
Kan du komme i tanke om andre felter? Måske et boolsk felt til at indeholde værdien sand eller falsk for at indikere om det er en snyde-terning, som kun kan få værdien 6? Måske et felt til at repræsentere den farve, der skal bruges, når der skrives ud? Måske et DateTime-felt som kan gemme information om, hvornår terningen sidst er rystet?
Felter er normalt en medlemstype, der gemmes væk for den, der benytter klassen ved at gøre dem private. På den måde kan du beskytte data, og selv sørge for at skabe tilgang gennem andre medlemstyper som eksempelvis metoder – eller som du skal se senere – egenskaber.
Info
Felter repræsenterer klassens data.
Felter er altså terningens data, og hvert objekt baseret på klassen har disse data placeret i hukommelsen. Det er ikke noget, du tager dig af – det klarer runtime.
I relation til klassen Terning kan vi lige nu nøjes med ét felt kaldet værdi og gøre dette felt til et heltal. Feltet er privat, hvilket betyder, at det kun kan tilgås inde fra klassens andre medlemmer. Det indikerer jo, at klassen nu mangler nogle offentlige medlemmer, så man udefra kan tildele og aflæse en værdi.
Egenskaber
I relation til terningen har du dog et problem med feltet værdi. Det er jo af en heltalstype, og der findes ingen typer, som kun tillader værdien 1-6. Det kan vi jo blot dokumentere os ud af ved at skrive, at ingen, der benytter vores terning, må tildele feltet værdi under 1 eller over 6. Men det var bedre, hvis terningen selv kunne holde styr på det, og det er her, egenskaber kommer ind i billedet. De kan bruges til at beskytte et felt og afvikle kode ved tildeling og ved aflæsning af feltet. Således kan vi skabe en egenskab kaldet Værdi (stort V i modsætning til feltet som staves med lille v), og skrive kode som sikrer, at der kun kan tildeles 1-6 ved, at en for lille eller høj værdi resulterer i en fejl (exception). Du kunne også tilføje kode, der afvikles, når feltet værdi aflæses. Hvis der er tale om en snydeterning, skal den eksempelvis returnere værdien 6 og ikke værdien af feltet. I større systemer er koden i egenskaberne også tit relateret til eksempelvis sikkerhed og log.
Info
Egenskaber beskytter klassens felter ved tildeling og aflæsning.
Egenskaberne beskytter altså typisk felterne, og giver en masse fordele som vi kommer nærmere ind på. I grundlæggende C# hænger et felt sammen med en tilhørende egenskab, og Microsoft foreslår en navngivningsstandard hvor feltet er stavet med lille og egenskaben med stort.
Metoder
I en klasse (eller struktur) kan du ligeledes tilføje metoder med samme syntaks, som du har set tidligere i bogen. Metoderne er typisk relateret til klassen felter (data), men behøver ikke at være det.
Info
Metoder repræsenterer klassens funktionalitet og er tit relateret til klassens felter.
I Terning-klassen er i hvert fald én metode nødvendig. Der skal være en mulighed for at ryste terningen, hvilket i kode svarer til at tildele feltet værdi et tilfældig tal mellem 1 og 6. Men der kunne godt være andre metoder. Hvad med en boolsk metode der fortæller, om terningen har en given værdi, en metode der udskriver værdien eller metoder, der gemmer/henter værdien fra en fil. Der er masser af muligheder.
Hændelser
Noget mere avanceret er den medlemstype, der kaldes en hændelse (event). For at forstå denne medlemstype er det igen vigtigt, at du kan se klassen Terning fra to udvikleres synspunkt – den udvikler, der skriver klassen Terning, og den udvikler, som benytter klassen Terning.
Hændelser er en mulighed for, at objekter af en klasse selv kan afvikle en metode når en eller anden hændelse sker, og denne metode er blevet tildelt af den, der benytter klassen Terning. Du kan med et godt programmeringsbegreb sige, at hændelser giver mulighed for at afkoble funktionalitet. Det er ikke udvikleren af klassen, der bestemmer, hvad der skal ske – men brugeren af klassen.
I relation til terningen kunne det måske være smart, at objekter af klassen terning kunne afvikle en metode, når værdien ”rystes” til en 6’er. Så kunne en udvikler, der bruger terningen, måske få applikationen til at give lyd, når det bliver en 6’er, og en anden kunne måske gøre noget grønt på en brugerflade. Pointen er, at udvikleren af terningen ikke skal skrive koden til en given hændelse – det er op til brugeren af terningen.
Info
En hændelse giver brugeren af en klasse mulighed at få afviklet en eller flere metoder, når en given hændelse finder sted.
Du kan med rette tænke, at brugeren af terningen jo blot selv kan skrive koden til at kontrollere om værdien er en 6’er og så selv afvikle sin egen kode. Og det er helt korrekt, men du skal tænke på, at der kan være situationer, hvor det kan være svært at vide, hvor man skal kontrollere en værdi, og så er alternativet et uendeligt loop eller en timer, der hele tiden kontrollerer værdier. Der kan også ligge en kompliceret logik bag en hændelse, og det derfor giver god mening at have kodet dette i terningen selv.
Konstruktører
Det kan tit være nødvendigt at afvikle kode, når et objekt bliver oprettet.
Der kan både være tale om kode relateret til initialisering (alle felter bliver sat til default værdier, og det er måske ikke optimalt), sikkerhed, log og så videre. Dette kan ske i en klasses konstruktører. Du kan vælge at tilføje konstruktører både med og uden argumenter således, at du kan oprette objekter på flere måder.
I relation til terningen kan man forestille sig, at klassen har to konstruktører.
Hvis brugeren af klassen Terning opretter et objekt uden argumenter, sørger en konstruktør for at kalde en metode, som tildeler et tilfældigt tal til feltet værdi. På den måde sikrer du, at feltet værdi ikke har en værdi på 0, hvilket jo ikke giver mening for en terning.
Info
En konstruktør er en samling instruktioner, der afvikles, når der bliver skabt et objekt af en klasse.
Hvis brugeren af klassen Terning opretter et objekt med et heltal som argument, kan en konstruktør sørge for at kontrollere om argumentet er mellem 1 og 6, og så tildele argumentet til feltet værdi. Så kan brugeren oprette en terning uden, at den får en tilfældig værdi.
Destruktør
Det modsatte af en konstruktør er en destruktør, som afvikler kode, når objektet fjernes fra hukommelsen af runtime (reelt af en komponent i frameworket kaldet en Garbage collector).
Det er sjældent, man benytter destruktører i C#, fordi runtime er så god til at rydde op efter sig. Der kan være enkelte situationer, hvor det er nødvendigt, men i grundlæggende C# kan du se helt bort fra dem. De gør typisk mere skade end gavn, fordi de nemt kan benyttes forkert.
I relation til terningen giver det heller ikke nogen mening af tilføje en destruktør.
Info
Destruktører benyttes ikke særlig meget i grundlæggende C# – blandt andet fordi runtime stiller andre features til rådighed, hvis du ønsker at afvikle oprydningskode.
Eksempler på brug af medlemstyper
Indtil nu har kapitlet bestået af en masse teori, og det kan måske give mere mening, hvis du ser lidt kode. Du skal ikke fokusere på, hvordan klassen er skrevet her, det lærer du i de efterfølgende kapitler, men mere på brugen af de forskellige medlemmer.
Her er eksempelvis brugen af de to konstruktører:
// Her oprettes en terning, og den konstruktør uden
// argumenter bliver afviklet automatisk. Det betyder,
// at Ryst-metoden bliver kaldt automatisk, og feltet
// værdi dermed allerede ved oprettelse bliver tildelt
// et tilfældigt tal mellem 1-6
Terning t1 = new Terning();
// Her oprettes en terning, og den konstruktør med et enkelt
// argumentet bliver afviklet automatisk. Det betyder,
// at feltet værdi dermed bliver tildelt værdien 2
Terning t2 = new Terning(2);
// Array af terninger som alle rystede
Terning[] bæger = new Terning[5];
for (int i = 0; i < 5; i++) {
bæger[i] = new Terning();
Her er eksempler på brug af egenskaben Værdi:
// Her oprettes en terning med værdien 3
Terning t1 = new Terning(3);
// Her sættes terningens værdi til 4
t1.Værdi = 4;
// Her aflæses terningens værdi
Console.WriteLine(t1.Værdi);
// Dette vil skabe en fejl fordi
// værdi skal være >=1 og <=6
t1.Værdi = 7;
Her benyttes terningens metode:
Terning t1 = new Terning();
// Her "rystes" terningen og får dermed en ny værdi
t1.Ryst();
Console.WriteLine(t1.Værdi);
Sluttelig er her et eksempel på brugen af hændelsen:
Terning t1 = new Terning();
// Metoden Beep bindes til ErSekser-hændelsen
t1.ErSekser += Beep;
for (int i = 0; i < 10; i++)
{
Console.WriteLine(t1.Værdi);
t1.Ryst();
}
// Denne metode afvikles automatisk når
// værdien bliver en sekser
void Beep(object? sender, EventArgs? e)
{
// Her kan være alt mulig kode
Console.Beep();
}
Eksemplet er lidt mere avanceret, og du skal ikke forstå syntaksen. Det som er væsentligt er, at brugeren af klassen Terning bestemmer, hvad der skal ske, når der rystes til en sekser, og klassen sørger selv for at få det til at ske.
Koden bag Terning-klassen
Hvis du har lyst til at prøve førnævnte kode af kan du kopiere denne klasse. Igen - se bort fra syntaks og teori lige nu. Det kommer senere:
public class Terning
{
private int værdi;
public int Værdi
{
get
{
return værdi;
}
set
{
værdi = value;
if (værdi < 1 || værdi > 6)
throw new Exception("Forkert værdi");
}
}
private static readonly Random random = new();
public event EventHandler? ErSekser;
public Terning()
{
Ryst();
}
public Terning(int værdi)
{
this.Værdi = værdi;
CheckErSekser();
}
public void Ryst()
{
this.Værdi = random.Next(1, 7);
CheckErSekser();
}
private void CheckErSekser()
{
if (Værdi == 6 && ErSekser != null)
ErSekser(this, EventArgs.Empty);
}
}
Summering
Brug terningen til at huske på at:
- Du kan bruge klasser til at simulere, emulere, abstrahere og repræsentere en entitet. Klassen Terning er en måde at beskrive en terning i kode.
- En af styrkerne ved objektorienteret programmering er genbrug af kode. Hvis først du har skabt klassen Terning, kan den benyttes i alle mulige terningespil, og i den mere avancerede C# kan du sågar arve funktionalitet til andre klasser. Mere om det senere.
- Felter er objekternes data. I eksemplet med terningen gemmes værdien i et felt.
- Egenskaber bruges til at beskytte felterne mod forkerte værdier og forkert aflæsning. I eksemplet med terningen beskytter egenskaben kaldet Værdi feltet kaldet værdi ved, at egenskaben er offentlig (og dermed kan tilgås udefra), og feltet er privat.
- Metoder er en måde at arbejde med objekternes felter. I eksemplet med terningen bruges Ryst-metoden til at skabe et tilfældigt tal.
- Hændelser giver mulighed for, at objekter afvikler metoder, når en hændelse sker. I eksemplet med terningen giver klassen mulighed for at tilføje en reference til en metode, der afvikles, når der rystes en sekser.