02 Juli 2025

C# 11 und 12 Beste Funktionen: Weniger schreiben, mehr erreichen

In diesem Artikel werfen wir einen Blick auf einige der neuen Funktionen, die in C# 11 und 12 eingeführt wurden und die dazu beitragen, Ihren Code zu vereinfachen und die Entwicklung reibungsloser zu gestalten. Diese Updates sind vielleicht nicht revolutionär, aber sie sind praxisnah und darauf ausgelegt, Zeit zu sparen, indem sie unnötige Komplexität reduzieren. Wir werden sehen, wie kleine Änderungen zu saubereren, effizienteren Lösungen bei alltäglichen Programmieraufgaben führen können.

Rohzeichenkette-Literale

Das Erstellen von Zeichenketten mit komplexem Inhalt war in C# historisch gesehen eine Herausforderung. Entwickler mussten häufig mit dem Maskieren von Sonderzeichen, Zeilenumbrüchen und Anführungszeichen kämpfen, was zu umfangreichem und oft unlesbarem Code führte. Dieser Prozess wird besonders mühsam, wenn Formate wie JSON, XML oder reguläre Ausdrücke direkt in Quelldateien eingebettet sind.

C# 11 hat Rohzeichenkette-Literale eingeführt, um diesen Schmerzpunkt direkt anzugehen. Diese Funktion ermöglicht es, dass Zeichenketten über mehrere Zeilen gehen und praktisch jedes Zeichen enthalten können, einschließlich eingebetteter Anführungszeichen und Backslashes, ohne dass Escape-Sequenzen benötigt werden. Ein Rohzeichenkette-Literal beginnt und endet mit mindestens drei doppelten Anführungszeichen (""").

Vor C# 11:

string oldJson = "{\r\n  \"name\": \"Alice\",\r\n  \"age\": 30\r\n}";
Console.WriteLine(oldJson);

Mit C# 11:

string newJson = """
  {
    "name": "Alice",
    "age": 30
  }
  """;
Console.WriteLine(newJson);

Jeglicher Leerraum vor den schließenden Anführungszeichen definiert die minimale Einrückung für die Zeichenkette, die der Compiler aus der endgültigen Ausgabe entfernt. Rohzeichenkette-Literale verbessern die Lesbarkeit von Zeichenketten erheblich und reduzieren die Wahrscheinlichkeit von Syntaxfehlern.

Listenmuster

Das Musterabgleichen in C# hat sich erheblich weiterentwickelt, wobei C# 11 Listenmuster einführt, um Sequenzabgleiche innerhalb von Arrays oder Listen zu ermöglichen. Diese Erweiterung erlaubt es Entwicklern, die Struktur und den Inhalt von Sammlungen prägnant und ausdrucksstark zu überprüfen.

Früher erforderte die Validierung der Sammlungsstruktur manuelle Überprüfungen der Länge und einzelner Indizes, was zu umfangreichem und weniger wartbarem Code führte. Listenmuster adressieren dies, indem sie Unter-Muster wie konstante, Typ-, Eigenschafts- und relationale Muster unterstützen. Zu den Hauptfunktionen gehören das Verwerfen-Muster (_) zum Abgleichen eines beliebigen einzelnen Elements und das Bereichs-Muster (..) zum Abgleichen einer beliebigen Sequenz von null oder mehr Elementen.

Vor C# 11:

int[] numbers = { 1, 2, 3 };

if (numbers != null && numbers.Length == 3 &&
    numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
    Console.WriteLine("Array enthält genau 1, 2, 3.");
}

if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
    Console.WriteLine("Array hat 2 als zweites Element.");
}

Mit C# 11:

int[] numbers = { 1, 2, 3 };

if (numbers is [1, 2, 3])
{
    Console.WriteLine("Array enthält genau 1, 2, 3.");
}

if (numbers is [_, 2, ..])
{
    Console.WriteLine("Array hat 2 als zweites Element.");
}

Listenmuster vereinfachen die Sequenzvalidierung in eine kompakte, lesbare Form und reduzieren erheblich die Anzahl der benötigten Codezeilen für solche Operationen.

Erforderliche Mitglieder

Die Objektinitialisierung kann manchmal zu einem unerwünschten Zustand führen, in dem wesentliche Eigenschaften oder Felder unzugeteilt bleiben. Traditionell erzwingen Entwickler eine obligatorische Initialisierung durch Konstruktoren, die alle erforderlichen Parameter akzeptieren, oder durch das Hinzufügen von defensiven Überprüfungen innerhalb von Methoden.

C# 11 führt den required-Modifikator für Eigenschaften und Felder ein, einen Mechanismus zur Durchsetzung zur Kompilierzeit. Wenn ein Mitglied als required markiert ist, stellt der Compiler sicher, dass es während der Objekterstellung einen Wert erhält, entweder durch einen Konstruktor oder einen Objektinitialisierer. Dies garantiert, dass Instanzen eines Typs immer in einem gültigen, vollständig initialisierten Zustand sind und verhindert häufige Fehler im Zusammenhang mit fehlenden Daten.

Vor C# 11:

public class OldPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void DisplayName() => Console.WriteLine($"Name: {FirstName} {LastName}");
}

// Verwendung:
var person = new OldPerson(); // Kein Kompilierfehler, aber es wird ein potenziell ungültiges Objekt erstellt
person.DisplayName();

Mit C# 11:

public class NewPerson
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    public void DisplayName() => Console.WriteLine($"Name: {FirstName} {LastName}");
}

// Verwendung:
// var person = new NewPerson(); // Kompilierfehler - fehlende erforderliche Eigenschaften
// var person = new NewPerson { FirstName = "John" }; // Kompilierfehler - LastName fehlt
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // OK
person.DisplayName();

Erforderliche Mitglieder eliminieren Überraschungen zur Laufzeit, indem sie die Initialisierung zur Kompilierzeit erzwingen und den Bedarf an manuellen Überprüfungen reduzieren. Diese Funktion erhöht die Zuverlässigkeit des Codes mit weniger defensivem Programmieren, sodass sich Entwickler auf die Funktionalität statt auf die Validierung konzentrieren können.

Primäre Konstruktoren

C# 12 führt primäre Konstruktoren für alle Klassen und Strukturen ein und erweitert eine Funktion, die einst exklusiv für Record-Typen war. Dies ermöglicht es, Konstruktorparameter direkt in der Typdefinition zu deklarieren, wodurch sie automatisch als Felder oder Eigenschaften über die gesamte Klasse hinweg sichtbar werden. Im Gegensatz zu traditionellen Konstruktoren überspringt dieser Ansatz explizite Felddeklarationen und manuelle Zuweisungen.

Das Hauptproblem, das hier gelöst wird, ist der wiederholte Standardcode bei der Objektinitialisierung. Früher mussten Entwickler private Felder definieren und Konstruktorargumente explizit zuordnen, was den Code unnötig aufblähte. Primäre Konstruktoren vereinfachen dies, indem sie die Initialisierungslogik direkt in die Typsignatur einbetten.

Vor C# 12:

public class OldProduct
{
    private readonly int _productId;
    private readonly string _productName;

    public OldProduct(int productId, string productName)
    {
        _productId = productId;
        _productName = productName;
    }

    public string PrintDetails() => $"Produkt-ID: {_productId}, Name: {_productName}";
}

// Verwendung:
OldProduct oldProd = new OldProduct(101, "Laptop");
oldProd.PrintDetails();

Mit C# 12:

public class NewProduct(int productId, string productName)
{
    public string PrintDetails() => $"Produkt-ID: {productId}, Name: {productName}";
}

// Verwendung:
NewProduct newProd = new NewProduct(102, "Keyboard");
newProd.PrintDetails();

Primäre Konstruktoren machen die Definition datenzentrierter Typen unglaublich prägnant. Sie verbessern die Lesbarkeit, indem sie die wesentlichen Konstruktionsparameter direkt neben dem Typnamen platzieren und die Abhängigkeiten der Klasse oder Struktur auf einen Blick klar machen.

Sammlungsausdrücke

Das Initialisieren von Sammlungen in C# war historisch gesehen mit verschiedenen Syntaxen verbunden, abhängig vom Sammlungstyp, wie new List<T> { ... } für Listen oder new T[] { ... } für Arrays. Das Kombinieren oder Zusammenführen bestehender Sammlungen in eine neue erforderte oft iterative Schleifen oder LINQ-Methoden wie Concat(), was Overhead und Wortreichtum hinzufügte.

C# 12 führt Sammlungsausdrücke ein, eine einheitliche und prägnante Syntax zum Erstellen und Initialisieren einer breiten Palette von Sammlungstypen. Mit einer einfachen [...]-Syntax können Entwickler Arrays, Listen, Span<T> und andere sammlungsähnliche Typen erstellen. Das neue Spread-Element (..) ermöglicht das direkte Einbetten von Elementen aus bestehenden Sammlungen in einen neuen Sammlungsausdruck, wodurch die Notwendigkeit manueller Verkettungen entfällt.

Vor C# 12:

// Initialisieren verschiedener Sammlungen
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };

// Kombinieren von Sammlungen
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);

Console.WriteLine(string.Join(", ", allNumbers));

Mit C# 12:

// Initialisieren verschiedener Sammlungen
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];

// Kombinieren von Sammlungen mit dem Spread-Operator
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];

Console.WriteLine(string.Join(", ", allNumbers));

Sammlungsausdrücke reduzieren die Wortreichtum beim Initialisieren und Kombinieren von Sammlungen und liefern eine sauberere und intuitivere Syntax. Diese Effizienz beschleunigt das Programmieren und verbessert die Lesbarkeit, unterstützt das Prinzip, mit weniger Zeilen mehr Wirkung zu erzielen.

Standard-Lambda-Parameter

Lambda-Ausdrücke, ein Eckpfeiler der funktionalen Programmierung in C#, konnten historisch gesehen keine Standardwerte für ihre Parameter definieren. Wenn ein Lambda optionale Argumente behandeln oder Ersatzwerte bereitstellen musste, mussten Entwickler auf bedingte Logik innerhalb des Lambda-Körpers zurückgreifen oder mehrere Überladungen definieren, obwohl Lambdas Überladungen nicht direkt unterstützen.

C# 12 schließt diese Lücke, indem es Standardwerte für Parameter in Lambda-Ausdrücken erlaubt. Die Syntax und das Verhalten spiegeln die von Methoden- oder lokalen Funktionsparametern wider und bieten eine flüssigere und prägnantere Möglichkeit, flexible Lambda-Funktionen zu definieren.

Vor C# 12:

// Lambda ohne Standardparameter.
// Wenn ein Standardwert für 'y' gewünscht war, wurde oft ein Wrapper oder bedingte Logik benötigt:
Func<int, int, int> addOld = (x, y) => x + y;

Func<int, int> addWithDefaultOld = x => addOld(x, 10); // Eine gängige Umgehung

Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));

Mit C# 12:

// Lambda mit Standardparametern
Func<int, int, int> addNew = (x, y = 10) => x + y;

Console.WriteLine(addNew(5, 3)); // y ist 3
Console.WriteLine(addNew(5));    // y wird standardmäßig auf 10 gesetzt

Die Einführung von Standardparametern für Lambdas erhöht deren Flexibilität und Ausdruckskraft erheblich. Es reduziert den Bedarf an redundanten Lambda-Definitionen oder interner bedingter Logik.

Fazit

C# 11 und 12 liefern eine überzeugende Reihe von Funktionen, die dem Versprechen “Weniger schreiben, mehr erreichen” gerecht werden. Von den Rohzeichenkette-Literalen und Listenmustern in C# 11 bis zu den primären Konstruktoren und Sammlungsausdrücken in C# 12 adressieren diese Fortschritte echte Frustrationen im täglichen Programmieren. Sie entfernen unnötige Syntax, erhöhen die Lesbarkeit und erzwingen sicherere Muster, wodurch Arbeitsabläufe in der Softwareentwicklung und bei Code-Konvertierungsprojekten direkt verbessert werden. Jede Innovation – sei es die Durchsetzung erforderlicher Mitglieder oder die Vereinfachung von Sammlungs-Setups – reduziert Tastenanschläge, maximiert die Klarheit und minimiert das Fehlerrisiko.

In Verbindung stehende Artikel