Met de komst van C# versie 9.0 zijn er heel veel waardevolle toevoegingen gedaan aan de programmeertaal en in deze Blog serie zullen we inzoomen op een aantal belangrijke nieuwe features. Al eerder behandeld in deze serie is deel 3 “Top-level statements”, we gaan nu verder met het laatste deel: “Record types”.
Om C# 9.0 te kunnen gebruiken dien je eerst de .NET 5 runtime of SDK te installeren. Ga hiervoor naar https://dotnet.microsoft.com/download. Na installatie zal C# 9.0 de standaard C# versie zijn bij elk nieuw project gebaseerd op .NET 5.
Waarom?
In deel 2 van deze serie is al gewezen op init-only setters. Op een eenvoudige manier kun je daarmee aangeven dat properties als immutable behandeld moeten worden.
Met het nieuwe keyword record krijg je als programmeur de mogelijkheid om snel en overzichtelijk een complete class immutable te maken. Je kunt dit record dan instantiëren, waarbij de data initieel wordt gezet en daarna niet meer te wijzigen is.
Dit levert je een aantal voordelen op.
Denk aan toepassing van deze record types in een multi-threaded applicatie. Omdat het object immutable is, met andere woorden niet kan worden benaderd om wijzigingen door te voeren, kan er geen race condition optreden waarbij thread 1 gegevens leest terwijl thread 2 gegevens schrijft. Je code kan worden vereenvoudigd zonder risico op deadlocks of nog erger, berekeningen met een onverwachte uitkomst.
Een ander groot voordeel van een immutable record is dat je de betrouwbaarheid van je unittesten kunt vergroten. Stel je een applicatie voor waarbij een “ouderwetse” DTO-class door diverse methodes in een keten als parameter wordt gebruikt. Wanneer de inhoud van deze class mutable is, zul je bij het schrijven van een test op één van die methodes uit moeten zoeken of er methodes bestaan die gegevens in jouw DTO kunnen wijzigen. Dit kan namelijk impact hebben op het aantal scenario’s dat je zult moeten testen.
Een immutable record maakt het onmogelijk dat gegevens worden gewijzigd, waardoor je veilig deze uitzoekklus over kunt slaan.
Hoe?
Declareren en instantiëren
C# 9.0 biedt twee mogelijkheden om een record te declareren. Het meest eenvoudig is de volgende manier, waarbij je positional arguments toepast:
Deze methode is intuïtief en overzichtelijk. De C# compiler voegt boilerplate-code zoals een constructor met parameters en een deconstructor toe zodat je direct een Object van het type Car kunt aanmaken en gebruik kan maken van enkele handige uitbreidingen. Uit de intellisense blijkt dat de aangegeven property LengthInCm gebruikt maakt van init als setter en dus inderdaad immutabel is.
Er is géén parameterloze constructor aanwezig, je bent dus verplicht om de parameters via de constructor te vertrekken.
Veel controle over Racecar heb je natuurlijk niet, nuttige zaken als het inbouwen van validatie of het aangeven van een default value is zo niet mogelijk. Daarom is ook de meer klassieke ogende methode beschikbaar waardoor je bijvoorbeeld berekende properties of methodes kunt toevoegen.
Vergelijken van records
Een mooie eigenschap van een record is dat het zuiver gezien een reference type is, maar dat het zich gedraagt als een value type. Dit heeft een belangrijk effect op het vergelijken van instances van records.
Bij de vergelijking wordt namelijk – net als bij een echt value type – niet gekeken of de instances naar hetzelfde object verwijzen, maar of het type en de inhoud van de record instanties overeenkomen.
De bijgaande test brengt dit mooi in beeld.
Er worden 2 aparte instanties van PersonRecord aangemaakt en vervolgens vergeleken via de ==-operator en de AreEqual methode. Met classes zijn we gewend dat dit verschillende objecten zijn en daarom verwachten we dat de test faalt. Maar omdat records zich gedragen als value types slaagt deze test.
Muteren van een record
Hoewel een record immutable is, is het wel mogelijk een kopie te maken van een instantie en daarbij kleine wijzigingen aan te brengen met behulp van het keyword with op de volgende manier:
Verder?
Bij het record type krijg je nog een aantal andere methodes tot je beschikking.
ToString()
Deze methode levert automatisch de inhoud van het type en alle publieke properties.
Deconstructor()
De deconstructor geeft je de mogelijkheid om op de volgende manier de gegevens uit je record te verzamelen.