C# 9.0 bringt prägnante, unveränderbare Typen

Zusammen mit .NET 5.0 ist auch die Version 9.0 der Programmiersprache C# einsatzbereit. heise Developer stellt die wichtigsten neuen Sprachfeatures vor.

In Pocket speichern vorlesen Druckansicht 77 Kommentare lesen
Lesezeit: 15 Min.
Von
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

Für alle .NET-Entwickler, die noch mit dem klassischen .NET Framework 4.8 (oder noch älteren Versionen) arbeiten, gibt es leider eine traurige Nachricht vorweg: C# 9.0 kann dort nicht mehr verwendet werden. C# 9.0 läuft nur auf .NET 5.0 sowie .NET Core 3.0/3.1 sowie Xamarin.

Das Highlight schlechthin in C# sind Records, mit denen man auf prägnante Weise Klassen mit Wertesemantik definieren kann. Die folgende Programmcodezeile, die aussieht wie ein Konstruktor mit vorangestelltem Schlüsselwort record, deklariert eine Klasse Person mit drei Properties ID, Vorname, Name und Status (Letzteres mit Standardwert):

public record Person(int ID, string Vorname, string Name, string Status = "unbekannt");

Diese Record-Klasse kann man nun wie eine normale Klasse instanziieren:

Person hs = new Person(123, "Holger", "Schwichtenberg");

und die dort enthaltenen Daten über die automatisch erzeugten Properties verwenden:

Console.WriteLine($"Person #{hs.ID}: {hs.Vorname} {hs.Name}");

Einen Unterschied zu einer normalen Klasse merkt man aber sofort, wenn man versucht, einen Property-Wert zu ändern. Diese Zuweisung nach der Instanziierung

hs.Vorname = "Dr. Holger";

erlaubt der Compiler nicht mehr und quittiert das mit dem Fehler

"Error CS8852 Init - only property or indexer 'Person.Vorname' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor".

Die Instanzen einer so definierten Record-Klasse sind unveränderlich (immutable) nach der Instanzierung. Das neue Schlüsselwort record ist viel syntaktischer Zucker. Mit einem Decompiler wie dem kostenfreien ILSpy entlarvt man, dass in Wirklichkeit mehr als eine DIN-A4-Seite Programmcode entstanden ist (siehe Listing 1).

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

public class Person : IEquatable<Person>
{
    protected virtual Type EqualityContract
    {
        [System.Runtime.CompilerServices.NullableContext(1)]
        [CompilerGenerated]
        get
        {
            return typeof(Person);
        }
    }

    public int ID { get; init; }
    public string Vorname { get; init; }
    public string Name { get; init; }
    public string Status  { get; init; }

    public Person(int ID, string Vorname, string Name, string Status = "unbekannt")
    {
        this.ID = ID;
        this.Vorname = Vorname;
        this.Name = Name;
        this.Status = Status;
        base..ctor();
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Person");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }

    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("ID");
        builder.Append(" = ");
        builder.Append(ID.ToString());
        builder.Append(", ");
        builder.Append("Vorname");
        builder.Append(" = ");
        builder.Append((object?)Vorname);
…
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator !=(Person? r1, Person? r2)
    {
        return !(r1 == r2);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator ==(Person? r1, Person? r2)
    {
        return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
    }

    public override int GetHashCode()
    {
        return (((EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(ID)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Vorname)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Status);
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as Person);
    }

    public virtual bool Equals(Person? other)
    {
        return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<int>.Default.Equals(ID, other!.ID) && EqualityComparer<string>.Default.Equals(Vorname, other!.Vorname) && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<string>.Default.Equals(Status, other!.Status);
    }

    public virtual Person <Clone>$()
    {
        return new Person(this);
    }

    protected Person(Person original)
    {
        ID = original.ID;
        Vorname = original.Vorname;
        Name = original.Name;
        Status = original.Status;
    }

    public void Deconstruct(out int ID, out string Vorname, out string Name, out string Status)
    {
        ID = this.ID;
        Vorname = this.Vorname;
        Name = this.Name;
        Status = this.Status;
    }
}

Listing 1: Dekompilat des Record-Typen "Person" mit dem Decompiler ILSpy

Mehr zu .NET 5.0

betterCode() präsentiert: .NET 5.0 – Das Online-Event am 3. Dezember 2020

Das können Teilnehmer von Experten der .NET-Szene lernen:

  • Von .NET Framework über .NET Core zu .NET 5.0: Was bedeutet das für die Migration, und wie groß sind die Aufwände?
  • Was ist neu in .NET 5.0?
  • Neue Features: ASP.NET Core 5.0 und Blazor 5.0 kennen lernen
  • Die wichtigsten Sprachneuerungen in C# 9
  • Mobile Entwicklung mit .NET 5
  • OR-Mapping mit Entity Framework Core 5.0
  • WinUI 3 als Alternative zu WPF und UWP
  • Ausblick auf .NET 6.0