C# 9 Features

  1. Inleiding
  2. Target Framework
  3. Top Level Calls
  4. Init Only Setters
  5. Fit and Finish
  6. Target typing – new expression
  7. Pattern Matching
    1. Relational
    2. Logical
  8. Record Type
    1. Instantiate
    2. ToString override
    3. Equality Checks
    4. GetHashCode
    5. Deconstructor
    6. With
    7. Samengestelde eigenschap
    8. Methods
    9. Inheritance
  9. Slot

Inleiding

Microsoft komt om de zoveel tijd met een nieuwe versie van C# en zo is in november 2020 versie 9 uitgebracht. In deze post gaan we in op de volgende C# 9 features: Top Level Calls, Init Only Setters, Fit and Finish, Target typing new expression, Pattern Matching en Record types. De voorbeeldcode kun je in deze GitHub repository vinden in ConsoleAppProject\Program.cs.

up | down

Target Framework

We hebben in eerdere posts het één en ander besproken aan de hand van een C# Console App en dat gaan we in deze post ook doen.

We gebruiken voor de voorbeelden in deze post Visual Studio 2019. De versie van Visual Studio moet versie 16.8 of hoger zijn omdat die versies C# 9 ondersteunen. Verder wordt met de installatie van Visual Studio 2019 meestal ook .NET 5 (of hoger) mee geïnstalleerd zodat je geen extra dingen hoeft te doen voor het geïnstalleerd doen krijgen van die .Net versie op je computer.

Mogelijk staat bij de aanmaak van je Console applicatie de “Target Framework” nog op versie 3.x. Ga daarvoor naar de project file van je project (Solution Explorer > Edit Project File) en wijzig de versie van je Target Framework. De versie moet versie 5.0 zijn. Je moet ook Target Framework 5.0 of hoger hebben voor het kunnen gebruiken van de C# 9 features.

up | down

Top Level Calls

C# 9 biedt de mogelijkheid om gebruik te maken van Top Level Calls. We zien bijvoorbeeld deze boilerplate code zodra we een Console applicatie project aangemaakt hebben:

Het begint met een Program-class met daarin een method Main en die Main-method is het startpunt / entry point van je applicatie. We zien in dit geval dat we 12 regels code hebben voor het laten doen tonen van die ene Console.WriteLine().

Je kunt in C#9 de hele Main-method achterwege laten door van die ene Console.WriteLine() een top Level Call te maken zodat dit volstaat:

Als je argumenten meegeeft vanuit de command prompt dan is dat voor de Top Level Calls geen issue. De inkomende parameters zijn ook voor de Top Level Calls bereikbaar. In dit voorbeeld starten we de console app met een .cmd-bestand waarbij we twee argumenten meegeven (de getallen 1 en 2):

@echo off
   "C:\Blog\CSharp9\CSharp9Solution\
    ConsoleAppProject\bin\Debug\net5.0\
    ConsoleAppProject.exe" 1 2
pause

Je programma zal uiteraard meer bevatten dan alleen maar een eenzame Console.WriteLine() en je kunt bij wijze van spreken alles “Top Level Call” maken waarbij je alles als volgt in je Program.cs propt:

// top level calls
Console.WriteLine($"de eerste inkomende 
argument is {args[0]}.");

Console.WriteLine($"de tweede inkomende 
argument is {args[1]}.");

Console.WriteLine($"en bij elkaar opgeteld, 
{args[0]} + {args[1]} = " +
$"{ optellen(
    int.Parse(args[0]), 
    int.Parse(args[1])) 
  }.");

static double optellen(int x, int y)
{
    return x + y;
}

Met dit resultaat:

Top Level Calls mogen alleen maar voorkomen in één bestand en het idee daarachter is dat een programma maar één startpunt mag hebben. Als je gebruik gaat maken van Top Level Calls  dan  kun je die het beste opnemen in bestand Program.cs zijnde het bestand dat algemeen bekend staat als het bestand dat de startpunt van je applicatie bevat.

up | down

Init Only Setters

C# kent sinds versie 9 de init keyword en aan object properties met de init keyword mag alleen een waarde toegekend worden bij instantiatie. Met de init keyword heb je geen aparte constructor nodig en je kunt gebruik maken van object initializers.

We illustreren het één en ander met deze klasse waarbij we de init keyword gebruiken voor de ID. We definiëren geen constructor voor de klasse.

// Instantieer - object initializer
EIGENAAR eigenaar1 = 
   new EIGENAAR { 
        ID = 1, 
        Omschrijving = "Sandra's auto", 
        Regio = "Noord"
       };

// Dit mag niet want het is geïnstantieerd
//eigenaar1.ID = 2;

public class EIGENAAR
{
    public int ID { get; init; }
    public string Omschrijving { get; set; }
    public string Regio { get; set; }
}

Zo kan de ID een sleutelwaarde zijn waarbij de waarde wordt toegekend door een database. We gaan in de voorbeelden ervan uit dat de ID een dingetje van de database is waarbij het niet de bedoeling is dat we zelf aan de ID gaan prutsen.

We zien dat we een waarde kunnen toekennen aan de ID bij de object initializer, maar daarna niet meer, want we zien dat de ID niet meer gewijzigd mag worden na de instantiatie.

We krijgen van de compiler deze melding: “Init-only property or indexer ‘’EIGENAAR.ID’ can only be assigned in an object initializer, or on ‘this’ or ‘base’ in an instance constructor or an ‘init’ accessor.” zodra we de waarde van de ID proberen te wijzigen na de instantiatie van het object.

up | down

Fit and Finish

Het instantiëren kan als volgt en dat gaat op zich prima, maar we moeten twee keer aangeven dat de EIGENAAR class een rol speelt in het gebeuren:

EIGENAAR eigenaar2 = 
   new EIGENAAR 
       { 
        ID = 2, 
        Omschrijving = "Petra's auto", 
        Regio = "Midden" 
       };

Met de var hoef je maar één keer op te geven dat het gaat om een EIGENAAR class, maar men beschouwt een var als “less readable”:

var eigenaar3 = 
   new EIGENAAR 
       { 
        ID = 3, 
        Omschrijving = "Olga's auto", 
        Regio = "Zuid" 
       };

In C# 9 kan een object als volgt geïnstantieerd worden:

EIGENAAR eigenaar4 = 
   new() { 
          ID = 4, 
          Omschrijving = "Henk's auto", 
          Regio = "Midden" 
         };

Deze manier van instantiëren wordt “Fit and finish” genoemd. Je hoeft op deze manier maar één keer op te geven dat het gaat om een EIGENAAR class en je hoeft geen var te gebruiken.

up | down

Target typing – new expression

Het instantiëren van objecten kan nog beknopter. Je definieert een constructor en de volgorde van de parameters van de constructor bepaalt welke waarde bij welke eigenschap terecht komt (target typing).

public class EIGENAAR
{
 // Constructor
 public EIGENAAR() { }

 // Constructor
 public EIGENAAR(
          int id, 
          string omschrijving, 
          string regio)
 {
     ID = id;
     Omschrijving = omschrijving;
     Regio = regio;
 }

 public int ID { get; init; }
 public string Omschrijving { get; set; }
 public string Regio { get; set; }
}

Ook bij deze vorm van instantiëren moet je twee keer opgeven dat het gaat om een EIGENAAR class:

EIGENAAR eigenaar5 = 
new EIGENAAR(5, "Henk's auto", "Noord");

In C# 9 kun je als volgt “target typed” instantiëren waarbij je maar één keer een class hoeft op te geven:

EIGENAAR eigenaar6 = 
new(6, "Jan's auto", "Noord");

up | down

Pattern Matching

In C# 9 heeft men de Pattern Matching onder handen genomen en hopelijk wordt de leesbaarheid van de C# code ermee bevorderd.

Relational

De leesbaarheid van je code kan met de C# 9 relational patterns (>, <, >= en <=) aanzienlijk verbeteren. We kunnen bijvoorbeeld binnen een switch expression als volgt relational patterns gebruiken:

Console.WriteLine(
$"Bij een geleend bedrag van Eur 1000 
is het rentepercentage 
{ GetRentePercentage(1000) }%.");

Console.WriteLine(
$"Bij een geleend bedrag van Eur 3000 
is het rentepercentage 
{ GetRentePercentage(3000) }%.");

Console.WriteLine(
$"Bij een geleend bedrag van Eur 8000 
is het rentepercentage 
{ GetRentePercentage(8000) }%.");

Console.WriteLine(
$"Bij een geleend bedrag van Eur 12000 
is het rentepercentage 
{ GetRentePercentage(12000) }%.");

//-------------------------------
static double GetRentePercentage
       (double BedragLening)
{
  return BedragLening switch
  {
    > 0     and < 2500 => 13,
    >= 2500 and < 5000 => 15,
    >= 5000 and < 10000 => 18,
    _ => 25
  };
}

De staffel die wordt gehanteerd voor de bepaling van het rentepercentage is nu gemakkelijker te herkennen in methode GetRentePercentage.

up | down

Logical

In C# 9 kunnen deze logical patterns gebruikt worden: and, or,is en not en zo zal de expressie eruit zien als je geen C# 9 logical pattern gebruikt:

Console.WriteLine(
$"Bij een geleend bedrag 
  van Eur 200 is het rentepercentage 
  { GetRentePercentage(200) }%.");

double rentePercentage = 
GetRentePercentage(200);

if (rentePercentage <= 13 || 
    rentePercentage >= 25)
    Console.WriteLine(
    $"Rentepercentage {rentePercentage} 
    is niet de middenmoot want 
    het percentage is <= 13% of 
    groter dan 25%");

De expressie zal er als volgt uitzien als je C# 9 logical patterns gaat gebruiken:

if (rentePercentage is <= 13 
                    or >= 25)
    Console.WriteLine(
    $"Rentepercentage {rentePercentage} 
    is niet de middenmoot want 
    het percentage is <= 13% of 
    groter dan 25%");

We zien dat de expressie minder cryptisch oogt en het herhalen van de parameter waarmee wordt vergeleken (in dit voorbeeld parameter rentepercentage ) hoeft ook niet meer.

up | down

Record Type

C# is een object georiënteerde programmeertaal waarin je allerlei klassen kunt definiëren en objecten uit klassen geïnstantieerd worden. De properties van een object krijgen een waarde en je kunt de waarden naar goeddunken wijzigen. De objecten zijn “mutable”.

Het komt echter ook voor dat je juist geen “mutable” objecten wilt hebben. Je wilt een object dat alleen bij de instantiatie waarden krijgt waarna de waarden van de object properties niet meer gewijzigd mogen worden. M.a.w. je wilt een object dat “immutable” is.

Microsoft heeft in C# 9 de record type geïntroduceerd en records zijn speciaal ontworpen voor het kunnen instantiëren van objecten die immutable moeten wezen. We creëren een aantal objecten waarbij sommige objecten geïnstantieerd zijn uit een klasse en andere objecten uit een record type en we zullen de objecten in detail bekijken.

Record types kun je ook mutable maken, maar Record types zijn met name in het leven geroepen voor immutable objects en het wordt dan ook door deze en gene afgeraden om gebruik te maken van constructies waarmee record types alsnog mutable worden. We gaan in deze post dan ook niet in op het mutable maken van record type objecten.

up | down

Instantiate

We beginnen met de definitie van een klasse en we gebruiken class EIGENAAR zijnde de klasse die we eerder in deze post al gebruikt hadden. We instantiëren de volgende objecten uit class EIGENAAR:

EIGENAAR eigenaar7 = 
new(7, "Jan's auto", "Noord");

EIGENAAR eigenaar8 = 
new(7, "Jan's auto", "Noord");

We definiëren een record type en record type EIGENAARREC ziet er als volgt uit:

public record EIGENAARREC
(int ID, string Omschrijving, string Regio);

En we instantiëren de volgende objecten uit record type EIGENAARREC:

EIGENAARREC eigenaarrec1 = 
new(1, "Sandra's auto", "Noord");

EIGENAARREC eigenaarrec2 = 
new(1, "Sandra's auto", "Noord");

// eigenaarrec1.Omschrijving = "Petra's auto";

We kunnen de waarden van de record type objecten na instantiatie niet meer wijzigen. De objecten zijn immutable en we krijgen deze melding van de compiler zodra we iets proberen te wijzigen na de instantiatie : “Error CS8852 Init-only property or indexer ‘EIGENAARREC.Omschrijving’ can only be assigned in an object initializer, or on ’this’ or ‘base’ in an instance constructor or an ‘init’ accessor.”.

up | down

ToString override

We krijgen onderstaand te zien als we een Console.Writeline loslaten op een object dat geboren is uit een klasse.

Console.WriteLine(
$"ToString implementation class: 
{ eigenaar7 }");

En we krijgen alleen de naam van de klasse te zien uit welke het object is geïnstantieerd:

Blijkbaar heeft men voor de record type de .ToString()-methode anders geïmplementeerd. We krijgen alles van het record type object te zien zodra we een Console.Writeline loslaten op het object:

Console.WriteLine(
$"ToString implementation record:\n
{ eigenaarrec1 }");

en ook nog eens in een JSON-achtig formaat:

up | down

Equality Checks

Record type objecten eigenaarrec1 en eigenaarec2 zijn twee verschillende objecten met eenzelfde inhoud. Record type objecten gedragen zich als value type objecten en dat komt tot uiting als we een “Equals” doen:

Console.WriteLine(
$"Zijn de record objecten 
  gelijk aan elkaar 
  value equals: 
  { Equals(eigenaarrec1, 
           eigenaarrec2) }"
);

Er wordt alleen gekeken naar de inhoud om te bepalen of de objecten hetzelfde zijn en de Equals beschouwt de record type objecten als identiek. De inhoud van de objecten is weliswaar identiek, maar het zijn wel degelijk twee verschillende objecten die op verschillende plekken in het geheugen staan. Met een ReferenceEquals wordt ook gekeken naar de geheugenlocatie:

Console.WriteLine(
$"Zijn de record objecten 
  gelijk aan elkaar 
  reference equals: 
  { ReferenceEquals(eigenaarrec1, 
    eigenaarrec2) }"
);

en daar blijkt dan uit dat het toch twee verschillende objecten betreft:

De geheugenlocatie wordt altijd meegenomen bij de objecten die zijn geïnstantieerd uit een klasse. De == operator:

Console.WriteLine(
$"Zijn de class objecten 
  gelijk aan elkaar: 
  { eigenaar7 == eigenaar8 }"
);

geeft  dan ook de waarde false want het zijn, ondanks hun identieke inhoud toch twee verschillende  objecten.

up | down

GetHashCode

Hetzelfde verhaal voor de hashcode:

Console.WriteLine(
$"GetHashCode object eigennaar7: 
{ eigenaar7.GetHashCode() } ");

Console.WriteLine(
$"GetHashCode object eigennaar8: 
{ eigenaar8.GetHashCode() }");

Console.WriteLine(
$"GetHashCode object eigennaarrec1: 
{ eigenaarrec1.GetHashCode() } ");

Console.WriteLine(
$"GetHashCode object eigennaarrec2: 
{ eigenaarrec2.GetHashCode() }");

We krijgen voor de record type objecten met eenzelfde inhoud eenzelfde hashcode, maar de hashcode is weer anders voor de objecten die zijn geïnstantieerd uit een klasse en het maakt niet uit dat de inhoud van die objecten identiek is.

up | down

Deconstructor

We hebben een record type EIGENAARREC gedefinieerd en bij de definitie zijn de eigenschappen in een bepaalde volgorde opgegeven.

public record EIGENAARREC
(int ID, string Omschrijving, string Regio);

We kunnen voor een record type object een aantal variabelen vullen met de waarden uit de eigenschappen van dat object:

var (ID, Omschrijving, Regio) 
= eigenaarrec2;

Console.WriteLine(
$"Deconstructing 
  uit object eigenaarrec2:\n
  ID->{ID} 
  Omschrijving->{Omschrijving} 
  Regio->{Regio}
");

Daarbij wordt gebruik gemaakt van de volgorde waarin de eigenschappen zijn opgegeven bij de record type definitie (positional records):

In een klasse moet een aparte “deconstructor” geschreven worden om iets soortgelijks voor elkaar te krijgen, maar we hoeven zoiets niet te doen bij een record type omdat die out-of –the-box al een standaard deconstructor heeft.

up | down

With

Op record types gebaseerde objecten kunnen gekopieerd worden en alles wordt default overgenomen uit het origineel. Het kan echter voorkomen dat sommige eigenschappen waarden moeten krijgen die afwijken van het origineel en je kan voor zulke gevallen gebruik maken van de with keyword:

EIGENAARREC 
eigenaarrec3 = eigenaarrec2 
with
{
  Omschrijving = "Petra's auto"
};

Console.WriteLine(
$"Overgenomen van Sandra:\n
{ eigenaarrec3 }\n");

De code kan op die manier veel beknopter worden omdat je alleen maar aandacht hoeft te besteden aan de eigenschappen die een afwijkende waarde moeten krijgen. Itereren door alle eigenschappen van een object om iets gekopieerd te krijgen hoeft niet meer met de with keyword.

up | down

Samengestelde eigenschap

Record types kunnen bestaan uit eigenschappen die samengesteld zijn uit de waarden van andere eigenschappen. Je kunt samengestelde eigenschappen opnemen in je record type definitie.

public record EIGENAARREC(
int ID, 
string Omschrijving, 
string Regio)
{
  public string VolledigeOmschrijving 
  { 
    get => $"In regio { Regio } bevindt zich 
                      { Omschrijving }";
  }
};

In dit voorbeeld bestaat samengestelde eigenschap VolledigeOmschrijving uit de waarden van eigenschappen Regio en Omschrijving, maar een samengestelde eigenschap kan ook het resultaat zijn van een berekening waarbij gebruik wordt gemaakt van de waarden van andere eigenschappen.

up | down

Methods

Record types ondersteunen net als klassen methoden en zo is in onderstaand voorbeeld Methode Over() onderdeel van de record type definitie:

public record EIGENAARREC(
int ID, 
string Omschrijving, 
string Regio)
{
  public string VolledigeOmschrijving 
  { 
    get => $"In regio { Regio } bevindt zich 
                      { Omschrijving }";

    public string Over()
    {
      return $"De Id van {Omschrijving} is { ID }.";
    }
  }
};

Waarbij we de methode van de record typ object als volgt kunnen aanroepen:

Console.WriteLine(
$"Aanroep methode .Over():\n
{ eigenaarrec3.Over() }");

Met dit resultaat:

up | down

Inheritance

Overerving is ook mogelijk bij record types, maar record types kunnen alleen maar van record types erven en niet van klassen.  In onderstaand voorbeeld erven we uit een record type EIGENAARREC waarbij we eigenschap milieusticker toevoegen aan de kopie:

public record EIGENAARREC2(
  int milieusticker, 
  int ID, 
  string Omschrijving, 
  string Regio) 
  : EIGENAARREC(ID, 
                Omschrijving, 
                Regio); 

En we zien dat object eigenaarrec4:

EIGENAARREC2 eigenaarrec4 
= new(4, 1, "Marisca's auto", "Zuid");

Console.WriteLine(
$"Geërfd uit EIGENAARREC:\n
{ eigenaarrec4 }");

een milieusticker eigenschap heeft:

up | down

Slot

De belangrijkste CSharp 9 features hebben we in deze post besproken. De nadruk in versie 9 ligt op het beknopter en leesbaarder maken van de code en daar valt wel wat voor te zeggen.

De vraag is wat de meeste C# developers met die nieuwe features gaan doen. Gaan ze inderdaad de features gebruiken om zo te komen tot beter te doorgronden code? Of is een kleine verandering in de manier van programmeren al te veel gevraagd waarbij vasthouden aan een cryptische, voor anderen niet te doorgronden programmeerstijl toch gemakkelijker is? Ik sta zelf niet onwelwillend tegenover de nieuwe features en ik zal ze zeker gebruiken als ze de leesbaarheid van mijn code verbeteren.

Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over C# heb geschreven? Hit the C# button…

up

Laat een reactie achter

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *