Prägnanter Code mit C# 10

Seite 2: Auf oberster Ebene

Inhaltsverzeichnis

Listing 2 enthält keine Typdeklaration, sondern freien C#-Code, der beim Start der Anwendung läuft. Das Sprachfeature Top-Level-Statement ist jedoch nicht neu in C# 10, sondern seit C# 9.0 vorhanden. Erwähnenswert ist es dennoch, da Microsoft in den Projektvorlagen von .NET 6 extensiv Gebrauch davon macht.

Im Zusammenspiel mit C# 10 ist zu beachten, dass in der Datei mit dem Top-Level-Statement keine Namensraumdeklaration mit File-Scoped Namespaces erfolgen kann. Die Programmstartbefehle müssen in der Datei ebenfalls nach erlaubten Typdeklarationen stehen, und es darf pro Projekt nur in einer Datei freien Code geben.

Listing 2 beginnt mit dem neuen global using. Eine solche globale Using-Direktive gilt für alle Dateien in einem Projekt. Somit ist es nicht erforderlich, zu Beginn jeder Datei den Namensraum zu importieren. Daher verzichtet Listing 1 auf using System;.

global using darf, wie in Zeile 3 gezeigt, den Zusatz static aufweisen. Dadurch genügt im folgenden Programmcode WriteLine() statt Console.WriteLine(). Ebenso sind Aliasse erlaubt:

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

Ein globaler Namensraumimport darf jedoch nicht innerhalb eines mit Blocksyntax deklarierten Namensraums erfolgen.

Die globalen Namensraumimporte dürfen in eine separaten Datei ausgelagert sein, um die Importe aus dem aktiven Sichtfeld zu verbannen. Alternativ dazu lassen sich Namensräume in der Projektdatei .csproj global mit dem Tag <Using> in einer <ItemGroup> importieren – optional mit dem Zusatz Static="True" für statische Elemente:

<Project Sdk="Microsoft.NET.Sdk">
 �
 <ItemGroup>
  <Using Include="System.Runtime.InteropServices" ></Using>
  <Using Include="System.Console" Static="True"></Using>
  <Using Include="Heise.Developer" ></Using>
</ItemGroup>

</Project>

Die neuen Standardprojektvorlagen von Microsoft importieren einige Namensräumen der .NET-Klassenbibliothek implizit. Damit ist unter anderem die Anweisung global using System; hinfällig.

Die Liste zeigt die automatischen Namensraum-Importe in .NET 6 (Abb. 1).

(Bild: Microsoft)

Implizite Namensräume können Probleme verursachen, wenn im eigenen Code Klassennamen vorkommen, die es in der .NET-Klassenbibliothek in anderen Namensräumen gibt. Die impliziten Namensräume sind nur aktiv, wenn in der Projektdatei (.csproj) in einer <PropertyGroup> das Tag <ImplicitUsings>enable</ImplicitUsings> vorkommt. Der Wert disable schaltet die impliziten Namensräume vollständig ab und vermeidet damit Namenskonflikte. Alternativ lassen sich einzelne implizite Namensräume gezielt deaktivieren:

<ItemGroup>
  <Using Remove="System.Threading.Tasks" />
</ItemGroup>

Mit einer String-Interpolation lassen sich seit C# 6.0 die Zusammensetzung von Zeichenketten aus festen und variablen Bestandteilen übersichtlich realisieren. Allerdings ist erst mit dem aktuellen Release die String-Interpolation bei der Wertzuweisung an Konstanten erlaubt. Voraussetzung ist, dass alle verwendeten Platzhalter mit Konstanten befüllt werden. Ein Beispiel zeigen die Zeilen 9 bis 11 in Listing 2.

Die String-Interpolation ist nicht nur für Konstanten, sondern auch für Variablen in C# 10 deutlich schneller als bis zu C# 9.0, da Microsoft die Umsetzung durch den Compiler überarbeitet hat. Während das System vor C# 10 die Zeichenketten mit String.Format() und String.Concat() verbunden hat, arbeitet es unter der Haube neuerdings mit dem InterpolatedStringHandler, einer Variante eines String Builder.

Ab Zeile 14 verwendet Listing 2 die in Listing 1 deklarierte Record-Struktur. Zunächst erzeugt der Primärkonstruktor eine neue Instanz, die sich nicht verändern lässt. Um den Artikelstatus anzupassen, erstellt Zeile 18 über das in C# 9.0 eingeführten with einen Klon. Dieser erhält alle Werte aus dem Ursprungsobjekt, abgesehen von den durch die Anweisung with veränderten.

Zeile 19 liefert auf Basis der automatisch für die Record-Struktur generierte ToString()-Überschreibung folgende Ausgabe:

Autor { ID = 123, Name = Dr. Holger Schwichtenberg,
Artikelstatus = abgegeben,
ObjektErzeugungsZeitpunkt = 01.06.2006 }

Der genannte Termin ist übrigens der Tag, an dem der Autor dieses Artikels seinen ersten Developer-Blogeintrag auf heise.de veröffentlicht hat, noch vor der offiziellen Gründung von heise Developer am 2.12.2008.

Die Zeilen 21 bis 24 in Listing 2 zeigen die Dekonstruktion des Record-Strukturen-Typs Autor in die lokalen Variablen id und name, wobei erstere bereits im Vorfeld deklariert und letztere erst in der Dekonstruktion deklariert wird:

int�id;
(id,�string�name,�_)�=�hs2;

Dekonstruktion gibt es seit C# 7.0, und der Compiler generiert für den Code der Record-Struktur automatisch eine Deconstruct()-Methode. Mit dem Unterstrich entfallen Werte, die für den Aufruf irrelevant sind – im Beispiel den Artikelstatus). Neu in C# 10 ist die sogenannte Mixed Deconstruction, bei der reine Zuweisungen an bestehende Variablen und neue Variablendeklarationen mit Initialisierung in einer Zeile gemischt sind. Zuvor war nur eine der beiden folgenden Varianten erlaubt:

// Variante 1:
(int�id,�string�name,�_)�=�hs2;

// Variante 2:
int�id;
string�name;
(id,�name,�_)�=�hs2;

Die in C# eingeführte Vereinfachung beim Property Pattern (Typprüfung inklusive Wertvergleich) ist in den Zeilen 27 bis 31 in Listing 2 zu sehen. Neuerdings ist die Punktnotation erlaubt:

if�(o�is�Autor�{�ObjektErzeugungsZeitpunkt.Year:�2006�})

In C# 9.0 h�tte man umst�ndlicher Folgendes schreiben m�ssen:

if�(o�is�Autor�{�ObjektErzeugungsZeitpunkt:�{�Year:�2006�}�})

Um Missverständnisse zu vermeiden sei erwähnt, dass die Zeile object o = hs; lediglich dazu dient, das Property-Pattern zu zeigen. Wer Vergleiche mit einer typisierten Variable vornimmt, kann in C# weiterhin die normale Punktnotation nutzen:

if (hs2.ObjektErzeugungsZeitpunkt.Year == 2006) �

Zum Abschluss des Programmteils folgt in Zeile 34 noch der Aufruf der Methode CheckObjektErzeugungsZeitpunkt() mit Caller Argument Expression. Da die Angabe des frühesten Datums dem ersten Nachrichtenbeitrag auf heise.de am 17. April 1996 entspricht, kann kein Laufzeitfehler auftreten.

Auch bei den Lambda-Ausdrücken hat Microsoft in C# 10 syntaktische Verkürzungen eingeführt. Die Zeile

var�status�=�(Autor�a)�=>�
  $"Artikel�von�{a.Name}�ist�im�Status:�{a.Artikelstatus}";

hätte in C# 9 noch eine explizite Typdeklaration statt var erfordert:

Func<Autor,�string>�status�=�a�=>�
  $"Artikel�von�{a.Name}�ist�im�Status:�{a.Artikelstatus}";

Wenn der Compiler den gewünschten Rückgabetyp für ein var im Lambda-Ausdruck nicht automatisch herleitet, hilft die zusätzliche Rückgabetypangabe:

var�f1�=�byte�()�=>�42;�//�R�ckgabetyp�w�re�sont�int
var�f2�=�FileSystemInfo�()�=>�
  new�DirectoryInfo(@"c:\Windows");�//�w�re�sonst�DirectoryInfo

C# 10 erlaubt in einem Lambda-Ausdruck die Angabe von Annotationen beziehungsweise Attributen für Parameter und den Rückgabewert:

var�f3�=�[return:NotNull]�([SensitiveData]�string�name)�=>�
  "Hallo�"�+�name;

Nachdem die beiden Listings die wichtigsten Neuerungen in C# 10 enthalten, bleiben einige frische Features übrig:

- Während Klassen in C# seit jeher einen parameterlosen Konstruktor besitzen konnten, ist er für Strukturen erst seit C# 10 erlaubt. Entwicklerinnen und Entwickler können ihn manuell definieren. Alternativ erzeugt der Compiler einen parameterlosen Konstruktor, wenn im aufrufenden Code eine Initialisierung der Felder und Properties bei der Deklaration der Strukturen zu finden ist. Vor C# 10 waren solche Initialisierungen in Strukturen verboten.

  • Das Schlüsselwort with für das Klonen von Objekten ist in C# 10 nicht nur für Record-Klassen und Record-Strukturen, sondern auch für normale Strukturen erlaubt. Solche with-Ausdrücke wie in Zeile 19 von Listing 2 funktionieren allerdings nicht mit normalen Klassen, da diese eine Referenzsemantik und keine Wertesemantik haben. with-Ausdrücke erzeugen eine flache Kopie des Objekts: Alle Attribute des Objekts inklusive der Zeiger werden kopiert, nicht jedoch der Inhalt, auf den die Pointer zeigen.
  • Bereits in C# 9.0 ließen sich in einem Record-Typ Methoden überschreiben, auch wenn sie Teil der automatischen Codegenerierung für den Record waren wie bei ToString(). Damit wurde die automatische Implementierung außer Kraft gesetzt. Ab C# 10 ist ausschließlich für ToString() das Schlüsselwort sealed erlaubt. Damit kann ein Record-Typ verhindern, dass Kindtypen die Methode mit der automatischen Implementierung wieder überschreiben. Folglich gilt eine versiegelte ToString()-Implementierung automatisch für alle abgeleiteten Record-Typen. Das Sprachfeature funktioniert nur bei Record-Klassen, da Record-Strukturen nicht erben können.
  • Die in C# 9.0 eingeführten Source-Code-Generatoren können mit der neuen Schnittstelle Microsoft.CodeAnalysis.IIncrementalGenerator inkrementell und damit schneller arbeiten.