C# 10 Features

  1. Inleiding
  2. Nullable References
  3. Global Using Directive
  4. File Scoped Namespace
  5. Top Level Calls
  6. Record Struct
  7. Constant interpolated strings
  8. Extended Property Patterns
  9. Slot

Inleiding

Microsoft komt om de zoveel tijd met een nieuwe versie van C# en zo is in november 2021 .Net 6 met C# 10 uitgebracht.

We laten in deze post een aantal features de revue passeren die nieuw zijn in C# 10. Features waar we het over gaan hebben in deze post zijn: de Nullable References, de Global Using Directive, de filescoped namespace, de Record Struct, de Constant interpolated string en de Extended Property Pattern. Minimal APIs maken ook deel uit van de C# 10 features, maar zie daarvoor deze post.

We bespreken in deze post het één en ander aan de hand van een C# 10 Console App. De voorbeeldcode kun je in deze GitHub repository vinden.

Waarbij de Console App met deze output komt:

up | down

Nullable References

Dingen zijn anders in C# 10 en dat merken we als we op deze manier een klasse definiëren:

public class EIGENAAR
{
    public int ID { get; set; }

    public string Omschrijving { get; set; }

    public string Voornaam { get; set; }

    public string Achternaam { get; set; }

    public string Regio { get; set; }
}

We krijgen meteen de volgende Nullable references compiler warnings voor de string velden van de klassendefinitie:

Null Reference Exceptions zijn in veel C# programma’s een veel voorkomend probleem. Ze hebben in C# 10 dan ook besloten om wat strikter te zijn jegens Non-nullable (lege waarden zijn niet toegestaan) reference type variabelen. De string is een voorbeeld van zo’n reference type variabele.

In bovenstaande klassendefinitie zijn de velden gedefinieerd als Non-nullable string-velden. Dit houdt in dat dit soort reference type variabelen altijd voorzien moeten worden van een waarde en de compiler gaat daar sinds C# 10 dan ook over zeuren. Dit om af te dwingen dat je voorzieningen gaat treffen dat de desbetreffende variabelen van waarden worden voorzien zodat ze niet meer null (leeg) zijn. In het geval van een klasse kun je bijvoorbeeld via de constructor ervoor zorgen dat bij de instantiatie tot een object meteen waarden worden toegekend aan de desbetreffende eigenschappen van het object.

Wat we ook kunnen doen is conform het advies van de compiler de reference type variabelen nullable (lege waarden zijn toegestaan) maken (… string? Omschrijving …), maar in deze post willen we ons niet bezig houden met het nullable-purisme van C#10. We passen derhalve het projectbestand (regel 6-7) als volgt aan opdat Nullable references door de compiler genegeerd worden:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <!-- <Nullable>enable</Nullable> -->
    <Nullable>disable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference 
        Include="Newtonsoft.Json" 
        Version="13.0.1" />
  </ItemGroup>
</Project>

In Visual Studio 2019 had je de bug dat het TargetFramework (regel 4) niet klopte als je een .Net 5 Console App genereerde. De bug is blijkbaar verholpen in Visual Studio 2022 want bij het TargetFramework staat nu wel de juiste .Net versie (net6.0).

up | down

Global Using Directive

We definiëren op de old-school manier met de nodige ceremony en boilerplate code als volgt onderstaand programma dat een string deserialiseert naar een object. Voor het deserialiseren gebruiken we de Newtonsoft library.

using Newtonsoft.Json;

namespace ConsoleApp
{
 class Program
 {
    static void Main()
    {
      string json = @"{ 
      'ID'            : '1',
      'Omschrijving'  : 'Sandra\'s auto',
      'Voornaam'      : 'Sandra',
      'Achternaam'    : 'Janssens',
      'Regio'         : 'Noord'
    }";
    EIGENAAR eigenaar = 
    JsonConvert.DeserializeObject<EIGENAAR>(json);
    Console.WriteLine($"ID: {eigenaar.ID}.");
    Console.WriteLine($"Omschrijving: {eigenaar.Omschrijving}");
    Console.WriteLine($"Eigenaar:  {eigenaar.Voornaam} {eigenaar.Achternaam}");
    Console.WriteLine($"Regio: { eigenaar.Regio}");
  }
 }
}

public class EIGENAAR
{
   // klassendefinitie...

Het is niet ondenkbaar dat op meerdere plekken in onze “solution” gebruik wordt gemaakt van de Newtonsoft library en je moet dan op allerlei plekken in je solution de using Newtonsoft.Json;-statement opnemen (regel 1).

De Global Using Directive zorgt ervoor dat we maar op één plek hoeven te definiëren dat we gebruik maken van de Newtonsoft library:

global using Newtonsoft.Json;

Het definiëren van een Global Using Directive hadden we in de Program.cs kunnen doen. Het kan echter best zo zijn dat we in de toekomst gebruik gaan maken van meerdere libraries. We kiezen er dan ook voor om gebruik te maken van een bestand Usings.cs met daarin al onze (toekomstige) Global Using Directives.

up | down

File Scoped Namespace

In de oude strucuur hebben we namespace ConsoleApp als volgt gedefinieerd:

namespace ConsoleApp
{
 class Program
 {
    static void Main()
    {
       //... Code ...

En we hoeven met de File Scoped Namespace (regel 1) een keer minder in te springen. Alle beetjes helpen en dit is wederom een bijdrage van C# 10 voor het leesbaarder doen maken van de code:

namespace ConsoleApp;

class Program
{
  static void Main()
  {
    //... Code ...

up | down

Top Level Calls

Top level calls waren al geïntroduceerd in C# 9. We passen het toe op ons voorbeeldprogramma waardoor het programma er als volgt uit gaat zien zonder een Program-class en een Main-method en een File Scoped Namespace, maar met een Global Using Directive. We zien dat het programma door dat alles weer wat “slanker” wordt.

string json = @"{ 
       'ID'            : '1',
       'Omschrijving'  : 'Sandra\'s auto',
       'Voornaam'      : 'Sandra',
       'Achternaam'    : 'Janssens',
       'Regio'         : 'Noord'
}";
EIGENAAR eigenaar =
JsonConvert.DeserializeObject<EIGENAAR>(json);
Console.WriteLine($"ID: {eigenaar.ID}.");
Console.WriteLine($"Omschrijving: {eigenaar.Omschrijving}");
Console.WriteLine($"Eigenaar: {eigenaar.Voornaam} {eigenaar.Achternaam}");
Console.WriteLine($"Regio: { eigenaar.Regio}");

public class EIGENAAR
{
   // klassendefinitie...

up | down

Record Struct

Microsoft had in C# 9 de recordtype geïntroduceerd. De recordtype kan beschouwd worden als een “light weight” class dat net zoals de class een reference type is, maar met het grote verschil dat het zich gemakkelijker laat lenen voor het “immutable” doen zijn.

Weliswaar “light weight” en “immutable”, maar de in C# 9 geïntroduceerde recordtype blijft net zoals een class een reference type geheugenvariabele (reference type = verwijzend naar een geheugenadres waar het één en ander staat).

De class als geheugenvariabele kan in sommige gevallen van een te “zwaar” kaliber zijn en structs vormen dan een beter alternatief. De struct is handig als je bijvoorbeeld een value type geheugenvariabele nodig hebt zonder additionele logica, maar wel met gegroepeerde velden/eigenschappen. Een reden voor het doen gebruiken van value types is dat dat gunstiger is qua performance.

Je kan in C# 10 een recordtype als een struct declareren waardoor de recordtype de eigenschappen krijgt van een struct (en daarmee ook een value type geheugenvariabele wordt).

public readonly record struct EIGENAARSTRUCT (
  int ID, 
  string Omschrijving, 
  string Voornaam, 
  string Achternaam, 
  string Regio
);

Met de with statement kun je een kopie trekken van de record struct waarbij we de ID en de omschrijving van andere waarden voorzien:

var eigenaarstruct = new EIGENAARSTRUCT
(2, "Peter's auto", "Peter", "Veermans", "Midden");

var eigenaarstruct2 = eigenaarstruct with 
{ ID = 3, Omschrijving = "Peter's tweede auto" };

up | down

Constant interpolated strings

We hebben in het voorbeeld in deze post reeds gebruik gemaakt van interpolated strings (te herkennen aan de $). Het opnemen van een constant in een interpolated string was niet mogelijk, maar dat is met C# 10 veranderd:

const string RDW = "Dienst Wegverkeer";

Console.WriteLine
($"De {RDW} verzorgt de registratie van alle 
gemotoriseerde voertuigen en rijbewijzen in Nederland.");

We zien dat we in een interpolated string de constant kunnen opnemen.

up | down

Extended Property Patterns

We definiëren voor het doen toelichten van deze feature onderstaande klasse waarbij klasse BETAALGEGEVENS deel uitmaakt van klasse EIGENAAR2:

public class EIGENAAR2
{
    public int ID { get; set; }

    public string Omschrijving { get; set; }

    public string Voornaam { get; set; }

    public string Achternaam { get; set; }

    public string Regio { get; set; }

    public BETAALGEGEVENS betaalgegevens { get; set; } = 
    new BETAALGEGEVENS();
}

public class BETAALGEGEVENS
{
    public string Rekeningnummer { get; set; }

    public string IBAN { get; set; }
    public string BIC { get; set; }
    public string Tenaamstelling { get; set; }

}

We instantiëren uit klasse EIGENAAR2 object eigenaar2 en we kennen de volgende waarden toe aan het object:

EIGENAAR2 eigenaar2 = new EIGENAAR2();
eigenaar2.ID = 3;
eigenaar2.Omschrijving = "Olga's auto";
eigenaar2.Voornaam = "Olga";
eigenaar2.Achternaam = "Mulder";
eigenaar2.Regio = "Zuid";
eigenaar2.betaalgegevens.IBAN = "1234567";
eigenaar2.betaalgegevens.BIC = "BANKNL12ABC";
eigenaar2.betaalgegevens.Tenaamstelling = "O. MULDER";

Als we willen gaan zoeken op waarden in geneste klasse BETAALGEGEVENS dan moest dat zo:

if(eigenaar2 is { Omschrijving: "Olga's auto", 
   betaalgegevens : { IBAN: "1234567" } } )
{
  Console.WriteLine($"De motorrijtuigenbelasting van 
  {eigenaar2.Omschrijving} wordt betaald door 
  {eigenaar2.betaalgegevens.Tenaamstelling}.");
}

Met C#10 kan het nu ook op onderstaande manier wat weer accolades scheelt (regel 2). Alle beetjes helpen en dit is ook weer een bijdrage van C# 10 voor het leesbaarder doen maken van de code:

if (eigenaar2 is { Omschrijving: "Olga's auto", 
    betaalgegevens.IBAN: "1234567" })
{
  Console.WriteLine($"De motorrijtuigenbelasting van 
  {eigenaar2.Omschrijving} wordt betaald door 
  {eigenaar2.betaalgegevens.Tenaamstelling}.");
}

up | down

Slot

Een aantal CSharp 10 features hebben we in deze post besproken. Het beknopt houden en het leesbaar maken van een CSharp programma is zo te zien één van de doelen die Microsoft voor ogen heeft bij het doen uitbrengen van een nieuwe versie van C# en dat zien we ook bij C# 10.

Ook hier weer de vraag of de meeste C# developers iets 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

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