Prägnanter Code mit C# 10

Die zehnte Version der Programmiersprache C# bietet nicht nur neue Arten von Datentypen auf dem Stack, sondern vermindert auch die Tipparbeit.

Lesezeit: 14 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 282 Beiträge

(Bild: Shutterstock)

Von
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

C# 10 ist zeitgleich mit .NET 6 erschienen, lässt sich aber mit Einschränkungen in älteren .NET-Versionen nutzen, wenn auch nicht mit offiziellem Support von Microsoft: "C# 10 is supported only on .NET 6 and newer versions."

Der aktuelle C#-Compiler in der Version 4.0 ist Teil von Visual Studio 2022 und des .NET 6 SDK. Ein eigenständiger Download findet sich im NuGet-Paket Microsoft.Net.Compilers. Für C# 10 ist die Paketversion 4.0.1 oder höher zuständig. Der Compiler lässt sich sowohl mit Visual Studio for Mac 2022 als auch mit einer aktuellen Version von Visual Studio Code und anderen OmniSharp-kompatiblen Editoren verwenden.

C# 10 bietet als Syntax-Neuerungen unter anderem verkürzte Deklarationen und Importe von Namensräumen, vereinfachte Lambda-Ausdrücke und Property Patterns, veränderbare und unveränderbare Record-Struktur-Typen als Alternative zu klassenbasierten Records, konstante Interpolated Strings sowie die Mischung von Zuweisung und eine gemischte Dekonstruktion.

Alle wesentlichen Neuerungen sind im Listing 1 zu "Autor.cs" und Listing 2 zu "Program.cs" zu sehen, die mit Zeilennummer für die bessere Zuordnung zum Text wiedergegeben sind. Der Programmcode steht als Visual-Studio-Projektmappe zum Download zur Verfügung.

Vorweg sei erwähnt, dass der Autor dieses Beitrags die neuen Sprachfeatures vorstellt, ohne sie im Einzelnen zu bewerten. Er hält sich bewusst aus den teils hitzigen Diskussionen in der Entwicklergemeinde heraus, ob man bestimmte Sprachfeatures benutzen dürfe.

Listing 1 zeigt in Zeile 4 eine Namensraumdeklaration auf Dateiebene (File-Scoped Namespace). Anstelle der bisher zwingenden Deklaration eines Blocks in geschweiften Klammern

namespace Heise.Developer

{
 ...
}

genügt eine Zeile mit Semikolon am Ende und ohne Block. Die Namensraumdeklaration auf Dateiebene muss vor allen Typdeklarationen erscheinen. Die Vereinfachung basiert auf der Erkenntnis des C#-Entwicklungsteams, dass lediglich ein geringer Teil der öffentlich zugänglichen C#-Dateien auf GitHub Namespaces verwenden: "Measuring an even broader set of millions of C# files on GitHub shows literally 99.99% of files have just one namespace in them."

Ab Zeile 7 folgt in dem deklarierten Namensraum die Typdefinition für die neue Typart readonly record struct. In C# 9.0 hatte Microsoft Record-Typen eingeführt. Intern handelt es sich dabei um Klassen, die auf dem Heap liegen, aber eine Wertesemantik haben und im Standard unveränderbare Objekte erzeugen. In C# 10 gibt es nun zusätzlich:

  • record class ist gleichbedeutend mit record ohne Zusatz. Es entsteht wie bisher eine Klasse, also ein Referenztyp auf dem Heap. Alle per Primärkonstruktor erzeugten Properties haben einen Init Only Setter. Somit ist das Objekt immutable, sofern der Setter nicht explizit Properties hinzufügt.
  • record struct steht für eine Struktur – einen Wertetyp auf dem Stack, der implizit von System.ValueType erbt. Anders als bei einer record class haben alle per Primärkonstruktor erzeugten Properties einen normalen Setter. Das Objekt ist somit mutable.
  • readonly record struct beschreibt ebenfalls eine Struktur, der von System.ValueType erbt. Alle per Primärkonstruktor erzeugten Properties haben einen Init Only Setter: Das Objekt ist immutable.

Ein Primärkonstruktor nutzt die Schreibweise, bei der nach dem Klassennamen direkt die Konstruktorparameter aufgeführt sind:

public readonly record struct Autor(int ID, 
                    string Name, 
                    string Artikelstatus = "unbekannt") { }

Er erzeugt automatisch öffentliche Properties und weist die übergebenen Werte zu.

Der vom Compiler für readonly record struct generierte Code (Listing 3) ähnelt dem einer record class. Er bietet Properties mit Getter und Init Only Setter inklusive Equals()-Implementierung, Operator-Überladung für == und !=, Ausgabe aller Datenmitglieder bei ToString() sowie eine Deconstruct()-Implementierung.

Zu beachten ist, dass bei einem record struct die Fields und Properties explizit initialisert werden müssen, die nicht Teil des Primärkonstruktors sind. Bei einer readonly record struct müssen alle zusätzlichen Datenmitglieder den Zusatz readonly aufweisen. Eine Wertinitialisierung in der Deklaration ist wie bei allen Mitgliedern mit dem Zusatz readonly erlaubt. Im Gegensatz zu einer Record-Klasse kann eine Record-Struktur weder ein Eltern- noch ein Kindtyp sein.

Die zusätzliche Property CheckObjektErzeugungsZeitpunkt besitzt den Datentyp DateOnly, der ebenso wie TimeOnly neu in .NET 6 ist und den bisherige Datentyp DateTime erweitert, indem er lediglich das Datum beziehungsweise die Uhrzeit speichert. Auch wenn beide Typen streng genommen keine Sprachfeatures von C# 10 sind, passen sie in den Kontext des Beispiels.

Zusätzlich zu den in C# 5.0 eingeführten Caller-Info-Annotationen [CallerFilePath], [CallerLineNumber] und [CallerMemberName] kennt C# 10 neuerdings Caller Argument Expressions, mit denen eine Methode die Information erhält, welche Ausdrücke (Variablennamen beziehungsweise Formeln) hinter den vom Aufrufer übergebenen Werten stehen. Dafür dient in der Parameterliste die Annotation System.Runtime.CompilerServices.CallerArgumentExpressionAttribute, die seit .NET Core 3.0 existiert.

Die Methode in den Zeilen 15 bis 30 im Listing 1 besitzt vier Parameter: zwei "echte" Parameter und zwei Caller Argument Expressions für die ersten beiden Parameter. Die Caller Argument Expressions beziehen sich auf den Namen der Parameter. Leider muss man die Parameter als Zeichenkette angeben, und der Operator nameof() funktioniert nicht.

Der Aufruf der Methode erwartet nur die ersten zwei Parameter:

  obj.CheckObjektErzeugungsZeitpunkt(new DateOnly(2010,1,1), 
                         DateOnly.FromDateTime(DateTime.Now));

Die übrigen füllt der Compiler automatisch. Obigen Aufruf quittierte das Programm am 15. November mit folgender Laufzeitfehlermeldung:

01.06.2006 muss zwischen 01.01.2010 (new DateOnly(2010,1,1))
und 15.11.2021 (DateOnly.FromDateTime(DateTime.Now)) liegen!
(Parameter 'ObjektErzeugungsZeitpunkt')

Die Methode erfährt, dass der Aufruf beim ersten Parameter einen statischen Wert (1.1.2010) angegeben hat und beim zweiten Parameter immer das aktuelle Datum liefert.