Neuerungen in C# 9.0

Mit Version 9.0 wird die Sprache C# um die folgenden Features und Verbesserungen erweitert:

C# 9.0 wird in .NET 5 unterstützt. Weitere Informationen finden Sie unter C#-Sprachversionsverwaltung.

Sie können das neueste .NET SDK über die .NET-Downloadseite herunterladen.

Eintragstypen

C# 9.0 führt Datensatztypen ein. Sie verwenden das record-Schlüsselwort, um einen Verweistyp zu definieren, der integrierte Funktionalität zum Kapseln von Daten bereitstellt. Sie können Datensatztypen mit unveränderlichen Eigenschaften erstellen, indem Sie Positionsparameter oder Standardeigenschaftensyntax verwenden:

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; } = default!;
    public string LastName { get; init; } = default!;
};

Sie können auch Datensatztypen mit änderbaren Eigenschaften und Feldern erstellen:

public record Person
{
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;
};

Datensätze können zwar änderbar sein, sind jedoch primär dafür vorgesehen, unveränderliche Datenmodelle zu unterstützen. Der Datensatztyp bietet die folgenden Funktionen:

Sie können Strukturtypen verwenden, um datenzentrierte Typen zu entwerfen, die Wertgleichheit und wenig oder kein Verhalten bereitstellen. Bei relativ großen Datenmodellen haben Strukturtypen jedoch einige Nachteile:

  • Sie unterstützen keine Vererbung.
  • Sie sind weniger effizient bei der Bestimmung der Wertgleichheit. Bei Werttypen verwendet die ValueType.Equals-Methode Reflexion, um alle Felder zu suchen. Für Datensätze generiert der Compiler die Equals-Methode. In der Praxis ist die Implementierung von Wertgleichheit in Datensätzen messbar schneller.
  • In einigen Szenarien wird mehr Arbeitsspeicher verwendet, da jede Instanz über eine vollständige Kopie aller Daten verfügt. Datensatztypen sind Verweistypen, sodass eine Datensatzinstanz nur einen Verweis auf die Daten enthält.

Positionssyntax für die Eigenschaftendefinition

Sie können Positionsparameter verwenden, um die Eigenschaften eines Datensatzes zu deklarieren und die Eigenschaftswerte zu initialisieren, wenn Sie eine Instanz erstellen:

public record Person(string FirstName, string LastName);

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}

Wenn Sie die Positionssyntax für die Eigenschaftendefinition verwenden, erstellt der Compiler Folgendes:

  • Eine öffentliche, automatisch implementierte Init-only-Eigenschaft für jeden Positionsparameter, der in der Datensatzdeklaration bereitgestellt wird. Eine Init-only-Eigenschaft kann nur im Konstruktor oder mit einem Eigenschafteninitialisierer festgelegt werden.
  • Ein primärer Konstruktor, dessen Parameter mit den Parametern mit fester Breite der Datensatzdeklaration übereinstimmen
  • Eine Deconstruct-Methode mit einem out-Parameter für jeden Positionsparameter, der in der Datensatzdeklaration bereitgestellt wird.

Weitere Informationen finden Sie unter Positionssyntax im C#-Sprachreferenzartikel zu Datensätzen (Records).

Unveränderlichkeit

Ein Datensatztyp ist nicht notwendigerweise unveränderlich. Sie können Eigenschaften mit set-Zugriffsmethoden und Feldern deklarieren, die nicht readonly sind. Doch obwohl Datensätze änderbar sein können, vereinfachen sie die Erstellung unveränderlicher Datenmodelle. Eigenschaften, die Sie mit Positionssyntax erstellen, sind unveränderlich.

Unveränderlichkeit kann nützlich sein, wenn Sie möchten, dass ein datenzentrierter Typ threadsicher ist oder ein Hashcode in einer Hashtabelle unverändert bleibt. Dadurch können Fehler verhindert werden, die auftreten, wenn Sie ein Argument als Verweis an eine Methode übergeben und die Methode den Argumentwert unerwartet ändert.

Die für Datensatztypen eindeutigen Features werden von durch den Compiler synthetisierte Methoden implementiert, und keine dieser Methoden beeinträchtigt die Unveränderlichkeit durch Ändern des Objektzustands.

Wertgleichheit

Wertgleichheit bedeutet, dass zwei Variablen eines Datensatztyps gleich sind, wenn die Typen sowie alle Eigenschafts- und Feldwerte übereinstimmen. Bei anderen Verweistypen bedeutet Gleichheit Identität. Zwei Variablen eines Verweistyps sind demnach gleich, wenn sie auf dasselbe Objekt verweisen.

Im folgenden Beispiel wird die Wertgleichheit von Datensatztypen veranschaulicht:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    var phoneNumbers = new string[2];
    Person person1 = new("Nancy", "Davolio", phoneNumbers);
    Person person2 = new("Nancy", "Davolio", phoneNumbers);
    Console.WriteLine(person1 == person2); // output: True

    person1.PhoneNumbers[0] = "555-1234";
    Console.WriteLine(person1 == person2); // output: True

    Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}

In class-Typen könnten Sie Gleichheitsmethoden und -operatoren manuell überschreiben, um Wertgleichheit zu erzielen, aber das Entwickeln und Testen dieses Codes wäre zeitaufwändig und fehleranfällig. Wenn diese Funktionalität integriert ist, werden Fehler verhindert, die sich daraus ergeben würden, dass vergessen wird, benutzerdefinierten Überschreibungscode zu aktualisieren, wenn Eigenschaften oder Felder hinzugefügt oder geändert werden.

Weitere Informationen finden Sie unter Wertgleichheit im C#-Sprachreferenzartikel zu Datensätzen (Records).

Nichtdestruktive Mutation

Wenn Sie unveränderliche Eigenschaften einer Datensatzinstanz mutieren müssen, können Sie einen with-Ausdruck verwenden, um eine nichtdestruktive Mutation zu erzielen. Ein with-Ausdruck erstellt eine neue Datensatzinstanz, bei der es sich um eine Kopie einer vorhandenen Datensatzinstanz handelt, bei der bestimmte Eigenschaften und Felder geändert wurden. Mit der Objektinitialisierersyntax können Sie die zu ändernden Werte angeben, wie im folgenden Beispiel gezeigt:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
}

public static void Main()
{
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

    Person person2 = person1 with { FirstName = "John" };
    Console.WriteLine(person2);
    // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { PhoneNumbers = new string[1] };
    Console.WriteLine(person2);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { };
    Console.WriteLine(person1 == person2); // output: True
}

Weitere Informationen finden Sie unter Nichtdestruktive Mutation im C#-Sprachreferenzartikel zu Datensätzen (Records).

Integrierte Formatierung für die Anzeige

Datensatztypen verfügen über eine vom Compiler generierte ToString-Methode, die die Namen und Werte der öffentlichen Eigenschaften und Felder anzeigt. Die ToString-Methode gibt eine Zeichenfolge in folgendem Format zurück:

<Datensatztypname { <Eigenschaftsname> = Wert, Eigenschaftsname>> = <<Wert>>, <...}

Für Verweistypen wird der Typname des Objekts, auf das die Eigenschaft verweist, anstelle des Eigenschaftenwerts angezeigt. Im folgenden Beispiel ist das Array ein Verweistyp, sodass System.String[] anstelle der tatsächlichen Arrayelementwerte angezeigt wird:

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Weitere Informationen finden Sie unter Integriert Formatierung im C#-Sprachreferenzartikel zu Datensätzen (Records).

Vererbung

Ein Datensatz kann von einem anderen Datensatz erben. Ein Datensatz kann jedoch nicht von einer Klasse erben, und eine Klasse kann nicht von einem Datensatz erben.

Im folgenden Beispiel wird die Vererbung mit Positionseigenschaftensyntax veranschaulicht:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Damit zwei Datensatzvariablen gleich sind, muss der Laufzeittyp gleich sein. Die Typen der enthaltenden Variablen können unterschiedlich sein. Dies wird im folgenden Codebeispiel veranschaulicht:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(student2 == student); // output: True
}

Im Beispiel verfügen alle Instanzen über dieselben Eigenschaften und Eigenschaftswerte. student == teacher gibt jedoch False zurück, obwohl beide Variablen den Typ Person haben. Und student == student2 gibt True zurück, obwohl eine Instanz eine Person-Variable und eine Instanz eine Student-Variable ist.

Alle öffentlichen Eigenschaften und Felder sowohl von abgeleiteten als auch von Basistypen sind in der ToString-Ausgabe enthalten, wie im folgenden Beispiel gezeigt:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Weitere Informationen finden Sie unter Vererbung im C#-Sprachreferenzartikel zu Datensätzen (Records).

init-only-Setter

Init stellt nur einheitlichen Syntax bereit, um Elemente eines Objekts zu initialisieren. Eigenschafteninitialisierer verdeutlichen, welcher Wert welche Eigenschaft festlegt. Der Nachteil ist, dass diese Eigenschaften festlegbar sein müssen. Ab C# 9.0 können Sie init-Zugriffsmethoden anstelle von set-Zugriffsmethoden für Eigenschaften und Indexer erstellen. Aufrufer können diese Werte mithilfe der Syntax von Eigenschafteninitialisierern in Erstellungsausdrücken festlegen. Diese Eigenschaften sind jedoch nach Abschluss der Erstellung schreibgeschützt. Nur-init-Setter bieten Ihnen die Möglichkeit, den Zustand innerhalb eines bestimmten Zeitfensters zu ändern. Dieses Zeitfenster schließt sich nach Abschluss der Konstruktionsphase. Die Konstruktionsphase endet effektiv, nachdem die gesamte Initialisierung, einschließlich aller Eigenschafteninitialisierer und with-Ausdrücke, abgeschlossen wurde.

Sie können Nur-init-Setter in einem jedem Typ deklarieren, den Sie schreiben. Die folgende Struktur definiert z. B. eine Struktur zur Wetterbeobachtung:

public struct WeatherObservation
{
    public DateTime RecordedAt { get; init; }
    public decimal TemperatureInCelsius { get; init; }
    public decimal PressureInMillibars { get; init; }

    public override string ToString() =>
        $"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
        $"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure";
}

Aufrufer können die Werte mithilfe der Syntax von Eigenschafteninitialisierern festlegen und gleichzeitig die Unveränderlichkeit wahren:

var now = new WeatherObservation 
{ 
    RecordedAt = DateTime.Now, 
    TemperatureInCelsius = 20, 
    PressureInMillibars = 998.0m 
};

Ein Versuch, eine Beobachtung nach der Initialisierung zu ändern, führt zu einem Compilerfehler:

// Error! CS8852.
now.TemperatureInCelsius = 18;

Nur-init-Setter können nützlich sein, um Basisklasseneigenschaften von abgeleiteten Klassen festzulegen. Sie können auch mithilfe von Hilfsprogrammen abgeleitete Eigenschaften in einer Basisklasse festlegen. Positionelle Datensätze deklarieren Eigenschaften mithilfe von Nur-init-Settern. Diese Setter werden in with-Ausdrücken verwendet. Sie können Nur-init-Setter für jedes class-, struct- oder record-Element deklarieren, das Sie definieren.

Weitere Informationen finden Sie unter init (C#-Referenz).

Top-Level-Anweisungen

Anweisungen auf oberster Ebene entfernen unnötige Zeremonie aus vielen Anwendungen. Betrachten Sie das kanonische Programm "Hallo Welt!"

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Nur eine der Codezeilen ruft eine Aktion hervor. Mit allgemeinen Anweisungen können Sie all diese Codebausteine durch die using-Anweisung und die eine Zeile ersetzen, die die Aktion verursacht:

using System;

Console.WriteLine("Hello World!");

Wenn Sie ein einzeiliges Programm schreiben möchten, können Sie die using-Anweisung auch entfernen und den vollqualifizierten Typnamen verwenden:

System.Console.WriteLine("Hello World!");

Allgemeine Anweisungen dürfen nur einer Anwendungsdatei eingesetzt werden. Wenn der Compiler in mehreren Quelldateien allgemeine Anweisungen findet, führt dies zu einem Fehler. Ein Fehler wird ebenfalls zurückgegeben, wenn Sie allgemeine Anweisungen mit einer deklarierten Einstiegspunktmethode des Programms kombinieren (in der Regel eine Main-Methode). Sie können sich dies vorstellen, als ob eine Datei die Anweisungen enthält, die normalerweise in die Main-Methode einer Program-Klasse geschrieben werden.

Einer der häufigsten Anwendungsfälle für dieses Feature ist die Erstellung von Lehrmaterial. Anfänger-C#-Entwickler können die kanonische "Hallo Welt!" in einer oder zwei Codezeilen schreiben. Keiner der zusätzlichen Codebausteine ist erforderlich. Aber auch erfahrene Entwickler werden viele Verwendungsmöglichkeiten für dieses Feature finden. Allgemeine Anweisungen bieten skriptähnliche Experimentierfunktionen, ähnlich wie Jupyter Notebook-Instanzen. Allgemeine Anweisungen eignen sich auch hervorragend für kleine Konsolenprogramme und Hilfsprogramme. Azure Functions ist ein idealer Anwendungsfall für allgemeine Anweisungen.

Vor allem schränken allgemeine Anweisungen weder den Umfang noch die Komplexität einer Anwendung ein. Diese Anweisungen können auf jede beliebige .NET-Klasse zugreifen oder diese verwenden. Außerdem schränken sie nicht die Verwendung von Befehlszeilenargumenten oder Rückgabewerten ein. Allgemeine (top-level) Anweisungen können auf ein Zeichenfolgenarray namens args zugreifen. Wenn allgemeine Anweisungen einen ganzzahligen Wert zurückgeben, wird dieser Wert zum ganzzahligen Rückgabecode einer synthetisierten Main-Methode. Allgemeine Anweisungen können async-Ausdrücke enthalten. In diesem Fall gibt der synthetisierte Einstiegspunkt Task oder Task<int> zurück.

Weitere Informationen finden Sie unter Top-Level-Anweisungen im C#-Programmierleithandbuch.

Verbesserungen am Musterabgleich:

C# 9 enthält neue Verbesserungen am Musterabgleich:

  • Typmuster stimmen einem Objekt mit einem bestimmten Typ überein
  • Klammernde Muster erzwingen oder betonen die Rangfolge von Musterkombinationen
  • Konjunktive and Muster erfordern beide Muster, die übereinstimmen
  • Disjunktive or Muster erfordern ein Muster, das übereinstimmen soll
  • Negierte not Muster erfordern, dass ein Muster nicht übereinstimmt
  • Relationale Muster erfordern weniger als, größer als, kleiner oder gleich oder größer als oder gleich einer bestimmten Konstante.

Diese Muster erweitern die Mustersyntax. Sehen Sie sich die folgenden Beispiele an:

public static bool IsLetter(this char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

Mit optionalen Klammern, die verdeutlichen, dass and Vorrang vor or hat:

public static bool IsLetterOrSeparator(this char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

Einer der gängigsten Anwendungsfälle ist eine neue Syntax für NULL-Überprüfungen:

if (e is not null)
{
    // ...
}

Jedes dieser Muster kann in jedem Kontext verwendet werden, in dem Muster zulässig sind: is-Musterausdrücke, switch-Ausdrücke, geschachtelte Muster und das Muster einer case-Bezeichnung einer switch-Anweisung.

Weitere Informationen finden Sie unter Muster (C#-Referenz).

Weitere Informationen finden Sie in den Abschnitten Relationale Muster und Logische Muster des Artikels Muster.

Leistung und Interop

Diese drei neuen Features verbessern die Unterstützung für die native Interop und spezifische Bibliotheken, die eine hohe Leistung erfordern: ganze Zahlen mit nativer Größe, Funktionszeiger und das Auslassen des localsinit-Flags.

Ganze Zahlen mit nativer Größe, nint und nuint, sind ganzzahlige Typen. Sie werden durch die zugrunde liegenden Typen System.IntPtr und System.UIntPtr ausgedrückt. Der Compiler gibt zusätzliche Konvertierungen und Vorgänge für diese Typen als native ganze Zahlen aus. Integer mit nativer Größe definieren die Eigenschaften für MaxValue oder MinValue. Diese Werte können nicht als Kompilierzeitkonstanten ausgedrückt werden, da sie von der nativen Größe einer ganzen Zahl auf dem Zielcomputer abhängen. Diese Werte sind zur Laufzeit schreibgeschützt. Konstantenwerte können für nint in folgendem Bereich verwendet werden: [int.MinValue ... int.MaxValue]. Konstantenwerte können für nuint in folgendem Bereich verwendet werden: [uint.MinValue ... uint.MaxValue]. Der Compiler führt eine konstante Faltung aller unären und binären Operatoren mithilfe der Typen System.Int32 und System.UInt32 durch. Wenn das Ergebnis nicht in 32 Bits passt, wird der Vorgang zur Laufzeit ausgeführt und nicht als Konstante angesehen. Ganze Zahlen mit nativer Größe können die Leistung in Szenarios steigern, in denen ganzzahlige Mathematik intensiv angewendet und die schnellstmögliche Leistung benötigt wird. Weitere Informationen finden Sie unter den nint- und nuint-Typen.

Funktionszeiger bieten eine einfache Syntax für den Zugriff auf die IL-Opcodes ldftn und calli. Sie können Funktionszeiger mithilfe der neuen delegate*-Syntax deklarieren. Ein delegate*-Typ ist ein Typ von Zeiger. Bei einem Aufruf des delegate*-Typs wird calli verwendet. Dies ist ein Unterschied zu einem Delegaten, der callvirt für die Invoke()-Methode verwendet. Syntaktisch sind die Aufrufe identisch. Bei Aufrufen von Funktionszeigern wird die managed-Aufrufkonvention verwendet. Wenn Sie deklarieren möchten, dass Sie die unmanaged-Aufrufkonvention benötigen, müssen Sie nach der delegate*-Syntax das Schlüsselwort unmanaged einfügen. Andere Aufrufkonventionen können mithilfe von Attributen in der delegate*-Deklaration angegeben werden. Weitere Informationen finden Sie unter Unsicherer Code und Zeigertypen.

Schließlich können Sie System.Runtime.CompilerServices.SkipLocalsInitAttribute hinzufügen, um den Compiler anzuweisen, das localsinit-Flag nicht auszugeben. Dieses Flag weist die Common Language Runtime an, alle lokalen Variablen mit 0 (Null) zu initialisieren. Das localsinit-Flag ist das Standardverhalten von C# seit Version 1.0. Die zusätzliche Nullinitialisierung kann jedoch in einigen Szenarios zu nachweisbaren Leistungseinbußen führen, insbesondere wenn Sie stackalloc verwenden. In diesen Fällen können Sie SkipLocalsInitAttribute hinzufügen. Sie können die Klasse einer einzelnen Methode oder Eigenschaft, zu class/struct/interface oder sogar zu einem Modul hinzufügen. Dieses Attribut hat keine Auswirkung auf abstract-Methoden. Es beeinflusst den für die Implementierung generierten Code. Weitere Informationen finden Sie unter SkipLocalsInit-Attribut.

Diese Features können die Leistung in einigen Szenarios verbessern. Sie sollten jedoch nur nach einem sorgfältigen Leistungsvergleich vor und nach der Einführung eingesetzt werden. Code, der ganze Zahlen in nativer Größe enthält, muss auf mehreren Zielplattformen mit unterschiedlichen Größen von ganzen Zahlen getestet werden. Die anderen Features erfordern unsicheren Code.

Anpassen und Fertigstellen von Features

Viele der anderen Features helfen Ihnen, Code effizienter zu schreiben. In C# 9.0 können Sie den Typ in einem neuen new-Ausdruck weglassen, wenn der Typ des erstellten Objekts bereits bekannt ist. Die häufigste Anwendungsfall hierfür sind Felddeklarationen:

private List<WeatherObservation> _observations = new();

Der Zieltyp new kann auch verwendet werden, wenn Sie ein neues Objekt erstellen müssen, das als Argument an eine Methode übergeben werden soll. In diesem Fall können Sie eine ForecastFor()-Methode mit der folgenden Signatur implementieren:

public WeatherForecast ForecastFor(DateTime forecastDate, WeatherForecastOptions options)

Sie können sie wie folgt aufrufen:

var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());

Ein weiterer nützlicher Anwendungsfall für dieses Feature ist die Kombination mit Nur-init-Eigenschaften, um ein neues Objekt zu initialisieren:

WeatherStation station = new() { Location = "Seattle, WA" };

Mithilfe einer return new();-Anweisung können Sie eine Instanz zurückgeben, die vom Standardkonstruktor erstellt wurde.

Ein ähnliches Feature verbessert die Zieltypauflösung von bedingten Ausdrücken. Aufgrund dieser Änderung müssen die beiden Ausdrücke keine implizite Konvertierung von einem in den anderen aufweisen, sondern können beide über implizite Konvertierungen in einen Zieltyp verfügen. Diese Änderung wird Ihnen wahrscheinlich nicht auffallen. Was Sie bemerken werden, ist, dass einige bedingte Ausdrücke, die zuvor eine Umwandlung erforderten oder nicht kompiliert werden konnten, jetzt funktionieren.

Ab C# 9.0 können Sie Lambdaausdrücken oder anonymen Methoden den Modifizierer static hinzufügen. Statische Lambdaausdrücke entsprechen den lokalen static-Funktionen: Eine statische Lambdafunktion oder anonyme Methode kann weder lokale Variablen noch den Instanzzustand erfassen. Der Modifizierer static verhindert, dass versehentlich andere Variablen erfasst werden.

Kovariante Rückgabetypen flexibilisieren die Rückgabetypen von override-Methoden. Eine override-Methode kann einen Typ zurückgeben, der vom Rückgabetyp der überschriebenen Basismethode abgeleitet wurde. Dies kann sowohl für Datensätze als auch für andere Typen nützlich sein, die virtuelle Klon- oder Factorymethoden unterstützen.

Außerdem erkennen und verwenden foreach-Schleifen eine GetEnumerator-Erweiterungsmethode, die ansonsten das foreach-Muster erfüllt. Diese Änderung bedeutet, dass foreach mit anderen musterbasierten Konstruktionen, z. B. mit dem async-Muster, sowie der musterbasierten Dekonstruktion konsistent ist. In der Praxis bedeutet diese Änderung, dass Sie jedem Typ foreach-Unterstützung hinzufügen können. Sie sollten die Verwendung von „foreach“ jedoch auf die Fälle beschränken, in denen die Enumeration eines Objekts in Ihrem Softwareentwurf sinnvoll ist.

Sie können auch Ausschussvariablen als Parameter für Lambdaausdrücke verwenden. So müssen Sie das Argument nicht mehr benennen, und der Compiler muss es unter Umständen gar nicht verwenden. Sie nutzen einfach _ für alle Argumente. Weitere Informationen finden Sie im Abschnitt Eingabeparameter eines Lambdaausdrucks des Artikels Lambdaausdrücke.

Schließlich haben Sie nun die Möglichkeit, Attribute auf lokale Funktionen anzuwenden. Sie können beispielsweise Nullable-Attributanmerkungen auf lokale Funktionen anwenden.

Unterstützung für Code-Generatoren

Die beiden letzten Features dienen der Unterstützung von C#-Code-Generatoren. C#-Code-Generatoren sind eine Komponente, die Sie selbst schreiben können und die einem Roslyn-Analysetool oder einem Codefix ähnelt. Der Unterschied besteht darin, dass Code-Generatoren Code analysieren und im Rahmen der Kompilierung neue Quellcodedateien schreiben. Ein typischer Code-Generator durchsucht Code nach Attributen oder weiteren Konventionen.

Ein Code-Generator liest Attribute oder andere Codeelemente mithilfe der Roslyn-Analyse-APIs. Auf Grundlage dieser Informationen fügt er der Kompilierung neuen Code hinzu. Quell-Generatoren können nur Code hinzufügen. Sie sind nicht berechtigt, vorhandenen Code während der Kompilierung zu ändern.

Die beiden features, die für Codegeneratoren hinzugefügt wurden, sind Erweiterungen für teilweise Methodensyntax und Modul-Initializer. Zuerst zu den Änderungen an partiellen Methoden: Vor C# 9.0 waren partielle Methoden privat (private) und konnten weder einen Zugriffsmodifizierer angeben noch über eine leere Rückgabe (void) oder out-Parameter verfügen. Diese Einschränkungen führten dazu, dass der Compiler alle Aufrufe von partiellen Methoden entfernte, wenn keine Methodenimplementierung bereitgestellt wurde. In C# 9.0 werden diese Einschränkungen behoben. Deklarationen von partiellen Methoden müssen jetzt jedoch implementiert werden. Code-Generatoren können diese Implementierung bereitstellen. Damit kein Breaking Change eingeführt wird, befolgt der Compiler bei jeder partiellen Methode, die keinen Zugriffsmodifizierer aufweist, die alten Regeln. Wenn die partielle Methode den Zugriffsmodifizierer private enthält, unterliegt die partielle Methode den neuen Regeln. Weitere Informationen finden Sie unter Partielle Methode (C#-Referenz).

Das zweite neue Feature für Codegeneratoren ist Modul-Initializer. Modulinitialisierer sind Methoden, an die das Attribut ModuleInitializerAttribute angefügt wurde. Diese Methoden werden von der Runtime vor jedem anderen Feldzugriff oder Methodenaufruf innerhalb des gesamten Moduls aufgerufen. Ein Modulinitialisierer:

  • muss statisch sein
  • muss parameterlos sein
  • muss eine leere Rückgabe („void“) zurückgeben
  • darf keine generische Methode sein
  • darf nicht in einer generischen Klasse enthalten sein
  • muss für das Modul zugänglich sein, in dem er enthalten ist

Der letzte Aufzählungspunkt bedeutet, dass die Methode und die Klasse, in der die Methode enthalten ist, intern oder öffentlich sein müssen. Diese Methode darf keine lokale Funktion sein. Weitere Informationen finden Sie unter ModuleInitializer-Attribut.