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.
(Bild: Shutterstock)
- Dr. Holger Schwichtenberg
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.
Verkürzt und vereinfacht
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.
Namensraumdeklaration auf Dateiebene
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."
Neue Strukturen
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 mitrecord
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 vonSystem.ValueType
erbt. Anders als bei einerrecord class
haben alle per Primärkonstruktor erzeugten Properties einen normalen Setter. Das Objekt ist somit mutable.readonly record struct
beschreibt ebenfalls eine Struktur, der vonSystem.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.
Gut informierte Methoden
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.