© Quardia/shutterstock.com
C# Records

Datenklassen leicht gemacht mit C# 9


Mit dem finalen Release von .NET 5.0 bescherte uns Microsoft auch C# 9. Eine der neuen Funktionen sind C# Records, eine spezielle Form von Klassen. In diesem Artikel wird anhand zahlreicher Codebeispiele gezeigt, wann und wie sie eingesetzt werden können.

Wer schon länger mit C# entwickelt, hat mit Sicherheit bereits unzählige Male Klassen wie Hero aus Listing 1 geschrieben. Die Klasse verlangt im Konstruktor die Werte, die im jeweiligen Objekt gespeichert werden und bietet diese Werte über Read-only Properties an. Es ist fast schon verwunderlich, dass es in C# so lange gedauert hat, bis die Sprache ein spezielles Konstrukt bekommen hat, um diesen häufigen Anwendungsfall zu vereinfachen. Schließlich hat zum Beispiel TypeScript hier seit Jahren mehr zu bieten. Seit Erscheinen von C# 9 hat das Warten ein Ende. Records vereinfachen die Entwicklung von C#-Datenklassen erheblich. In diesem Artikel erfahren Sie, was Records sind, welche Möglichkeiten sie bieten und wie man sie in Verbindung mit bestehenden Bibliotheken wie Entity Framework oder ASP.NET verwendet.

Beispielcode

Dieser Artikel enthält viele Codebeispiele, da man ein C#-Sprachkonstrukt wie Records meiner Ansicht nach am besten anhand von Beispielen erklärt. Wer mit dem Beispielcode experimentieren möchte, der kann den jeweiligen Code in eine .NET-5-Konsolenanwendung kopieren. Alle Listings sind vollständig und können für sich allein ausprobiert werden. Damit der Code übersichtlich bleibt, wurde die neue Top-Level-Statements-Funktion von C# 9 verwendet, durch die man auf das Definieren einer Main-Funktion verzichten kann. Natürlich brauchen Sie den Code nicht abzutippen. Ich habe ein GitHub Gist [1] erstellt, das alle Codebeispiele enthält. Von dort können Sie den Code kopieren, den Sie probieren möchten.

Wer ganz genau wissen möchte, was hinter den Kulissen passiert, dem empfehle ich, die generierten Assemblies mit dnSpy [2] zu disassemblieren und sich den generierten IL-Code anzusehen.

Listing 1

var heroes = new Hero[] { new("Homelander", "DC", true), new("Jessica Jones", "Marvel", false), }; class Hero { public Hero(string name, string universe, bool canFly) => (Name, Universe, CanFly) = (name, universe, canFly); public string Name { get; } public string Universe { get; } public bool CanFly { get; } }

Records sind Klassen

Beginnen wir mit dem wahrscheinlich wichtigsten Hinweis: Records sind nur eine spezielle Form von Klassen. Listing 2 zeigt, wie die Hero-Klasse aus Listing 1 als Record geschrieben wird. Es konnte eine Menge Code eingespart werden. Sieht man sich den aus so einem Record erzeugten IL-Code an, wird klar, dass die ganze Magie im C#-Compiler steckt. Auf IL- und Runtime-Ebene gibt es keinen Record. Er wurde zu einer normalen .NET-Klasse, die aber spezielle Eigenschaften hat, auf die wir in Folge noch näher eingehen werden.

Listing 2

var heroes = new Hero[] {  // Note auto-generated constructor new("Homelander", "DC", true), new("Jessica Jones", "Marvel"), }; // Note auto-generated deconstructor var (name, universe, canFly) = heroes[0]; System.Console.WriteLine(name); record Hero(string Name, string Universe, bool CanFly = false);

Die Tatsache, dass Records eine spezielle Form von Klassen sind, hat einige Auswirkungen:

  • Viele Bibliotheken, die heute Klassen voraussetzen, können unverändert auch Records verarbeiten. Einige Beispiele dafür sehen wir später.

  • Man kann zusätzliche Member (z. B. Properties, Methoden) zu Records hinzufügen.

  • Wenn notwendig, kann man zusätzliche Konstruktoren hinzufügen.

  • Records können von anderen Records erben. Es ist jedoch nicht möglich, dass Records von Klassen erben oder umgekehrt.

  • Records können Interfaces implementieren.

Listing 3 zeigt, wie die Vererbung von Records sowie das Hinzufügen von Konstruktoren und Members funktionieren.

Listing 3

var heroes = new Hero[] { new("Homelander", "DC", true), new("Groot", "Marvel", false), new RealLifeHero("Superman", "DC", true, "Clark", "Kent"),  // Next object uses alternative constructor new RealLifeHero("Jessica", "Jones", "Marvel", false), }; interface IHero { string Name { get; } string Universe { get; } } // Records can implement interfaces record Hero(string Name, string Universe, bool CanFly) : IHero {  // Records can have methods just like regular classes public override string ToString() => $"{Name} ({Universe})"; } // Records can have base records record RealLifeHero(string Name, string Universe, bool CanFly, string RealLifeFirstName, string RealListLastName) : Hero(Name, Universe, CanFly) {  // Records can have additional members public string FullName => $"{RealLifeFirstName} {RealListLastName}";  // Records can have additional constructors that call the  // constructor generated for the record. public RealLifeHero(string firstName, string lastName, string universe, bool canFly) : this($"{firstName} {lastName}", universe, canFly, firstName, lastName) { } }

Unveränderbarkeit

Records haben im Vergleich zu Klassen eine Besonderheit: Sie sind standardmäßig immutable (= unveränderbar), der Objektinhalt kann also nach dem Erstellen eines Objekts nicht mehr geändert werden. Zu beachten ist aber, dass Records shallow immutable sind, nicht deep immutable. Wenn ein Record also eine Referenz auf ein mutable Objekt beinhaltet (z. B...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang