Records
En post (record) i C# er en af de features, som du har svært ved at undvære, når du først lige har lært, hvad de kan bruges til.
Information til undervisere
Det er en god idé at introducere records i forbindelse med simpel OOP. Det er en af de features, som du har svært ved at undvære, når du først lige har lært, hvad de kan bruges til. Udover det praktiske omkring syntaks bør immutable objekter også blive understreget.
Også godt at nævne datamapping i forbindelse med records, da det er en af de ting, som records er gode til.
I sin helt grundlæggende form kan du oprette brugen af record-kodeordet, som en hurtig måde at skabe en type til at opbevare data. Lad os antage, at du har behov for at opbevare data relateret til en person. Hvis du benytter en klasse eller en struktur, kunne du skrive kode som følger:
class Person
{
public string Navn { get; init; }
public int Alder { get; init; }
public bool ErDansk { get; init; }
public Person(string navn, int alder, bool erDansk)
{
Navn = navn;
Alder = alder;
ErDansk = erDansk;
}
}
Det er en simpel data-klasse, som kan benyttes som eksempelvis:
Bemærk, at klassen er immutable. Når egenskaber først er tildelt en værdi, kan de ikke tilrettes igen. Du ser tit denne form for meget simple datatyper, som blot har til formål at transportere data.
Definition af en record
Præcis samme type kan også oprettes som en post som følger:
I stedet for 12-13 linjers kode kan du nøjes med én linje. Resten klarer kompileren for dig. Den vil nemlig sørge for at autogenerere en klasse med de nævnte egenskaber, en konstruktør og en del andre medlemmer.
Info
Bemærk, at poster (records) er en C# 9 feature!
Oprettelse af et objekt sker på præcis samme måde som ved den førnævnte Person-klasse:
De autogenerede egenskaber Navn, Alder og ErDansk vil være init-egenskaber, og typen er dermed immutable:
Person p1 = new Person("Mikkel", 17, true);
// p1.Navn = "Mathias"; // init egenskab - kan ikke tildeles en værdi
Men udover at der bliver autogenereret egenskaber og konstruktør bliver der ligeledes tilføjet andre medlemmer. Eksempelvis er ToString-metoden overskrevet, så den returnerer en string, der beskriver værdierne:
Person p1 = new Person("Mikkel", 17, true);
Console.WriteLine(p1);
// Udskriver:
// Person { Navn = Mikkel, Alder = 17, ErDansk = True }
Hvis det er din egen klasse, skal du selv tilføje den kode. Ellers vil ToString-metoden blot returnere navnet på klassen. Yderligere er der tilføjet kode, der overskriver != og == operatorerne samt metoderne GetHashCode og Equals. Det betyder, at objekter kan sammenlignes på værdier og ikke på referencer:
Person p1 = new Person("Mikkel", 17, true);
Person p2 = new Person("Mikkel", 17, true);
Console.WriteLine(p1 == p2); // true
Hvis det er din egen klasse, skal du selv tilføje den kode.
Der er ligeledes autogenereret en Deconstruct-metode således, at følgende kode er mulig:
Person p1 = new Person("Mikkel", 17, true);
(var navn, var alder, var erDansk) = p1;
Console.WriteLine(navn); // Mikkel
Slutteligt er det nemt at få en kopi (husk – en post er immutabel) af objektet ved hjælp af with-kodeordet:
Person p1 = new Person("Mikkel", 17, true);
Console.WriteLine(p1);
// Person { Navn = Mikkel, Alder = 17, ErDansk = True }
Person p2 = p1 with { Alder = 18 };
Console.WriteLine(p2);
// Person { Navn = Mikkel, Alder = 18, ErDansk = True }
Bemærk, hvor nemt det er at kopiere data fra p1 til p2 og blot ændre alder.
Hvis du er nysgerrig på, hvordan den autogenererede klasse ser ud, så prøv at erklære en post på https://sharplab.io. Der kan du se både den dekompilerede IL kode og tilhørende C# kode.
En mutabel record
Selvom en post er immutabel, kan du godt lave en mutabel post. Det gør du ved selv at definere egenskaberne og konstruktøren. Her er et eksempel:
public record Person
{
public string? Navn { get; set; }
public int Alder { get; set; }
public bool ErDansk { get; set; }
public Person()
{
}
}
Nu kan du ændre på egenskaberne:
Person p1 = new Person { Navn = "Mikkel", Alder = 17, ErDansk = true };
p1.Alder = 18;
// eller
Person p2 = new Person();
p2.Navn = "Mikkel";
p2.Alder = 17;
p2.ErDansk = true;
Nu er det næsten at betragte som en almindelig klasse med mutable egenskaber, men med alle de andre fordele, som en post giver.
Tilføjelse af medlemmer
Du kan vælge at tilføje metoder og ekstra konstruktør samt egne egenskaber eller andre medlemmer, og en post kan også indgå i et arvehierarki med andre poster, men det ligger uden for denne bogs rammer at komme nærmere ind på det – bortset fra et simpelt eksempel:
record Person(string Navn, int Alder, bool ErDansk)
{
public int EstimeretFødselsår()
{
return DateTime.Now.Year - this.Alder;
}
}
Bemærk, at definitionen af typen nu benytter tuborgklammer for at gøre det muligt at tilføje en metode.
Person p1 = new Person("Mikkel", 17, true);
Console.WriteLine(p1);
// Person { Navn = Mikkel, Alder = 17, ErDansk = True }
Console.WriteLine(p1.EstimeretFødselsår());
// 2003
Helt overordnet skal du bare vide, at definition af en simpel datatype kan klares på en enkelt linje som en post, og samtidigt får du en masse yderligere funktionalitet foræret helt automatisk.
Opgaver
class eller struct
I C# 10.0 blev det muligt at skabe records som structs ved at tilføje kodeordet struct
(kodeorde class
kan undlades for at skabe en klassebaseret record).
- Record class: En record class er en reference type (HEAP), der er afledt af System.Object, ligesom en almindelig klasse. Den primære forskel er, at en record class er umodificerbar og genererer automatisk metoder som Equals, GetHashCode, og ToString baseret på de definerede egenskaber. Record classes understøtter også “value-based equality”, hvilket betyder, at to record-instanser anses for at være ens, hvis deres egenskaber har de samme værdier. En record class erklæres ved at bruge
record
-keywordet:
- Record struct: En record struct er en værditype (STACK), der er afledt fra System.ValueType, ligesom en almindelig struktur. Ligesom record classes er record structs umodificerbare og genererer automatisk metoder som Equals, GetHashCode, og ToString baseret på de definerede egenskaber. Record structs understøtter også “value-based equality”. En record struct erklæres ved at bruge både
record
ogstruct
-keywords:
En record struct er typisk en meget lille datatype.
Både record classes og record structs har fordele som umodificerbarhed, hvilket gør dem velegnede til funktionel programmering og trådsikkerhed. De genererer også automatisk metoder og understøtter “value-based equality”, hvilket reducerer den mængde kode, der skal skrives manuelt, og gør det lettere at sammenligne objekter.
Serialisering
Serialisering virker typisk fint i nyere frameworks. Her er et eksempel på JSON serialisering:
Person person = new("Tom", 29);
string json = System.Text.Json.JsonSerializer.Serialize(person);
Console.WriteLine(json);
Person? person1 = System.Text.Json.JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(person1?.Name);
public record Person(string? Name, int Age);
men det kan være lidt teknisk i andre biblioteker - her XML som kræver en default constructor og en attribut:
using System.Xml.Serialization;
const string path = @"c:\temp\person.xml";
Person person = new("Tom", 29);
XmlSerializer serializer = new(typeof(Person));
using (FileStream wfs = File.OpenWrite(path))
serializer.Serialize(wfs, person);
using FileStream rfs = File.OpenRead(path);
Person? person1 = serializer.Deserialize(rfs) as Person;
Console.WriteLine(person1?.Name);
[Serializable]
public record Person(string? Name, int Age)
{
public Person() : this(default, default)
{
}
}
DTO
DTO, som står for Data Transfer Object, er et designmønster anvendt i softwareudvikling for at overføre data mellem softwarekomponenter. DTO’er er særligt nyttige i lagdelte systemer, for eksempel i webapplikationer, hvor de kan bidrage til at adskille forskellige lag og dermed forenkle udvekslingen af data.
Fordele ved at bruge DTO’er:
-
Separation af bekymringer: DTO’er adskiller præsentationslaget fra forretningslogikken og datalaget. Dette øger modulariteten og forenkler vedligeholdelsen af systemet.
-
Reduceret netværksbelastning: Ved at sende kun de nødvendige data kan DTO’er hjælpe med at reducere størrelsen på netværksanmodninger, hvilket er afgørende for ydeevnen, især i distribuerede applikationer.
-
Sikkerhed: DTO’er kan bruges til at ekskludere følsomme data, der ikke skal eksponeres for klienten, hvilket forbedrer datasikkerheden.
-
Fleksibilitet i dataformatering: DTO’er gør det muligt at tilpasse dataformateringen til forskellige klienters behov uden at ændre den underliggende domænemodel.
Typisk er DTO’er immutable - altså objekter, hvis tilstand ikke kan ændres efter de er oprettet. Dette fører til mere forudsigelig og trådsikker kode, da du ikke behøver at bekymre dig om, at objektets tilstand ændres uventet. Records i C# er en elegant løsning til at oprette sådanne objekter. De giver en enkel syntaks for at definere dataholdere, og de er som standard immutable.
Forestil dig, at vi har en Person
-entitet i vores database, og vi vil eksponere nogle af disse data via et API, men ikke alle. Vi kan bruge en record til at definere en DTO, der indeholder de ønskede data.
// En entitet, der repræsenterer en person i databasen
public class Person
{
public int Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public DateTime BirthDate { get; set; }
// Yderligere felter...
}
// En record, der bruges som DTO for at sende persondata via API
public record PersonDTO(string FullName, string Email);
Så kan du bruge denne DTO i dit API ved at konvertere en Person
-entitet til en PersonDTO
-record - eksempelvis:
// Metode, der henter data fra databasen og konverterer til DTO
public PersonDTO GetPersonAsDTO(int id)
{
var person = database.GetPersonById(id); // Antag, at dette henter en Person fra databasen
return new PersonDTO(person.FullName, person.Email);
}
I dette eksempel bruges en Person
-klasse til at interagere med databasen, mens PersonDTO
-recorden bruges til at overføre data til API-klienten. Kun de nødvendige oplysninger (i dette tilfælde FullName
og Email
) inkluderes i DTO’en. Ved at bruge en record sikrer vi, at DTO’en er immutable, hvilket bidrager til en mere sikker og vedligeholdelsesvenlig kodebase.
Datamapping
Ovennævnte eksempel er meget simpelt med en enkelte mindre DTO. I virkeligheden vil du ofte have brug for at overføre data mellem flere forskellige typer. Det er her, datamapping kommer ind i billedet. Datamapping er processen med at overføre data fra et format eller et objekt til et andet. Det er en afgørende del af mange applikationer, især når data skal overføres mellem forskellige lag i en applikation, såsom fra en domænemodel til en datatransferobjekt (DTO) eller vice versa. Datamapping kan udføres manuelt eller ved hjælp af biblioteker, der automatiserer processen.
Se datamapping for mere information.