Kapitel 1. Einführung in C# und .NET

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

C# ist eine allgemeine, typsichere, objektorientierte Programmiersprache. Das Ziel der Sprache ist die Produktivität der Programmierer. Zu diesem Zweck bietet C# ein ausgewogenes Verhältnis zwischen Einfachheit, Ausdruckskraft und Leistung. Der Hauptarchitekt der Sprache seit ihrer ersten Version ist Anders Hejlsberg (Schöpfer von Turbo Pascal und Architekt von Delphi). Die Sprache C# ist plattformneutral und funktioniert mit einer Reihe von plattformspezifischen Laufzeiten.

Objektorientierung

C# ist eine reichhaltige Umsetzung des objektorientierten Paradigmas, das Kapselung, Vererbung und Polymorphismus umfasst. Verkapselung bedeutet, dass ein Objekt abgegrenzt wird, um sein externes (öffentliches) Verhalten von seinen internen (privaten) Implementierungsdetails zu trennen. Im Folgenden werden die besonderen Merkmale von C# aus objektorientierter Sicht beschrieben:

Vereinheitlichtes Typensystem
Der grundlegende Baustein in C# ist eine gekapselte Einheit von Daten und Funktionen, die als Typ bezeichnet wird. C# hat ein einheitliches Typensystem, in dem alle Typen letztlich einen gemeinsamen Basistyp haben. Das bedeutet, dass alle Typen, unabhängig davon, ob sie Geschäftsobjekte darstellen oder primitive Typen wie Zahlen sind, die gleiche Grundfunktionalität haben. Eine Instanz eines beliebigen Typs kann zum Beispiel durch den Aufruf der Methode ToString in eine Zeichenkette umgewandelt werden.
Klassen und Schnittstellen
In einem traditionellen objektorientierten Paradigma ist die einzige Art von Typ eine Klasse. In C# gibt es mehrere andere Arten von Typen, darunter auch eine Schnittstelle. Eine Schnittstelle ist wie eine Klasse, die keine Daten enthalten kann. Das bedeutet, dass sie nur das Verhalten (und nicht den Zustand) definieren kann, was eine Mehrfachvererbung und eine Trennung zwischen Spezifikation und Implementierung ermöglicht.
Eigenschaften, Methoden und Ereignisse
Im rein objektorientierten Paradigma sind alle Funktionen Methoden. In C# sind Methoden nur eine Art von Funktionsmitgliedern, zu denen auch Eigenschaften und Ereignisse gehören (es gibt auch andere). Eigenschaften sind Funktionsmitglieder, die einen Teil des Zustands eines Objekts kapseln, z. B. die Farbe einer Schaltfläche oder den Text eines Etiketts. Ereignisse sind Funktionsmitglieder, die das Handeln bei Änderungen des Objektzustands vereinfachen.

Obwohl C# in erster Linie eine objektorientierte Sprache ist, macht sie auch Anleihen beim funktionalen Programmierparadigma:

Funktionen können als Werte behandelt werden
Mit Delegates können in C# Funktionen als Werte an und von anderen Funktionen übergeben werden.
C# unterstützt Muster für Reinheit
Der Kern der funktionalen Programmierung besteht darin, die Verwendung von Variablen, deren Werte sich ändern, zu vermeiden und stattdessen deklarative Muster zu verwenden. C# verfügt über wichtige Funktionen, die bei diesen Mustern helfen, z. B. die Möglichkeit, unbenannte Funktionen zu schreiben, die Variablen "einfangen"(Lambda-Ausdrücke), und die Möglichkeit, Listen- oder reaktive Programmierung über Abfrageausdrücke durchzuführen. C# bietet außerdem Datensätze, die das Schreiben unveränderlicher (schreibgeschützter) Typen erleichtern.

Typ Sicherheit

C# ist in erster Linie eine typensichere Sprache. Das bedeutet, dass Instanzen von Typen nur über die von ihnen definierten Protokolle interagieren können, wodurch die interne Konsistenz jedes Typs sichergestellt wird. C# verhindert zum Beispiel, dass du mit einem String-Typ interagieren kannst, als wäre er ein Integer-Typ.

Genauer gesagt unterstützt C# statische Typisierung, d.h. die Sprache erzwingt Typsicherheit zur Kompilierzeit. Dies geschieht zusätzlich zur Typsicherheit, die zur Laufzeit erzwungen wird.

Die statische Typisierung beseitigt eine große Anzahl von Fehlern, bevor ein Programm überhaupt ausgeführt wird. Sie verlagert die Last von den Unit-Tests zur Laufzeit auf den Compiler, um zu überprüfen, ob alle Typen in einem Programm richtig zusammenpassen. Das macht große Programme viel einfacher zu verwalten, vorhersehbarer und robuster. Außerdem hilft dir die statische Typisierung mit Tools wie IntelliSense in Visual Studio beim Schreiben eines Programms, weil es für eine bestimmte Variable weiß, welcher Typ sie ist und welche Methoden du daher für diese Variable aufrufen kannst. Solche Tools können auch überall in deinem Programm erkennen, wo eine Variable, ein Typ oder eine Methode verwendet wird, und ermöglichen so ein zuverlässiges Refactoring.

Hinweis

C# ermöglicht es auch, Teile deines Codes mit dem Schlüsselwort dynamic dynamisch zu typisieren. Dennoch bleibt C# eine überwiegend statisch typisierte Sprache.

C# wird auch als stark typisierte Sprache bezeichnet, weil die Typregeln streng durchgesetzt werden (entweder statisch oder zur Laufzeit). Du kannst zum Beispiel keine Funktion, die eine ganze Zahl akzeptiert, mit einer Fließkommazahl aufrufen, wenn du die Fließkommazahl nicht vorher explizit in eine ganze Zahl umwandelst. Das hilft, Fehler zu vermeiden.

Speicherverwaltung

C# verlässt sich bei der automatischen Speicherverwaltung auf die Laufzeitumgebung. Die Common Language Runtime verfügt über einen Garbage Collector, der als Teil deines Programms ausgeführt wird und den Speicher für Objekte, die nicht mehr referenziert werden, zurückfordert. Dadurch muss der Programmierer den Speicher für ein Objekt nicht mehr explizit freigeben, und das Problem der falschen Zeiger, das in Sprachen wie C++ auftritt, entfällt.

C# schafft Zeiger nicht ab: Es macht sie lediglich für die meisten Programmieraufgaben überflüssig. Für leistungsrelevante Hotspots und Interoperabilität sind Zeiger und explizite Speicherzuweisungen in Blöcken erlaubt, die mit unsafe gekennzeichnet sind.

Plattform Unterstützung

C# hat Laufzeiten, die die folgenden Plattformen unterstützen:

  • Windows 7+ Desktop (für Rich-Client-, Web-, Server- und Befehlszeilenanwendungen)

  • macOS (für Web- und Befehlszeilenanwendungen und Rich-Client-Anwendungen über Mac Catalyst)

  • Linux (für Web- und Kommandozeilenanwendungen)

  • Android und iOS (für mobile Anwendungen)

  • Windows 10-Geräte (Xbox, Surface Hub und HoloLens) über UWP

Es gibt auch eine Technologie namens Blazor, die C# zu einer Web-Assembly kompilieren kann, die in einem Browser läuft.

CLRs, BCLs und Laufzeiten

Die Laufzeitunterstützung für C#-Programme besteht aus einer Common Language Runtime und einer Base Class Library. Eine Runtime kann auch eine übergeordnete Anwendungsschicht enthalten, die Bibliotheken für die Entwicklung von Rich-Client-, Mobil- oder Web-Anwendungen enthält (siehe Abbildung 1-1). Es gibt unterschiedliche Laufzeiten für verschiedene Arten von Anwendungen und verschiedene Plattformen.

Runtime architecture
Abbildung 1-1. Laufzeitarchitektur

Common Language Runtime

Eine Common Language Runtime (CLR) bietet wichtige Laufzeitdienste wie die automatische Speicherverwaltung und die Ausnahmebehandlung. (Das Wort "Common" bezieht sich auf die Tatsache, dass dieselbe Runtime auch von anderen verwalteten Programmiersprachen wie F#, Visual Basic und Managed C++ genutzt werden kann).

C# wird als verwaltete Sprache bezeichnet, weil es den Quellcode in verwalteten Code kompiliert, der in Intermediate Language (IL) dargestellt wird. Die CLR wandelt den IL-Code in den nativen Code der Maschine um, z. B. X64 oder X86, normalerweise kurz vor der Ausführung. Dies wird als Just-In-Time (JIT) Kompilierung bezeichnet. Die Ahead-of-Time-Kompilierung ist auch verfügbar, um die Startzeit bei großen Assemblies oder ressourcenbeschränkten Geräten zu verkürzen (und um die Regeln des iOS App Stores bei der Entwicklung mobiler Apps zu erfüllen).

Der Container für verwalteten Code wird Assembly genannt. Eine Assembly enthält nicht nur AWL, sondern auch Typinformationen(Metadaten). Durch das Vorhandensein von Metadaten können Assemblies auf Typen in anderen Assemblies verweisen, ohne dass zusätzliche Dateien benötigt werden.

Hinweis

Mit dem Tool ildasm von Microsoft kannst du den Inhalt einer Assembly untersuchen und disassemblieren. Und mit Tools wie ILSpy oder dotPeek von JetBrain kannst du noch weiter gehen und die IL in C# dekompilieren. Da IL eine höhere Ebene als nativer Maschinencode ist, kann der Decompiler das ursprüngliche C# ziemlich gut rekonstruieren.

Ein Programm kann seine eigenen Metadaten abfragen(reflection) und sogar neue IL zur Laufzeit erzeugen(reflection.emit).

Basisklassenbibliothek

Eine CLR wird immer mit einer Reihe von Assemblies ausgeliefert, die Base Class Library (BCL) genannt werden. Eine BCL stellt Programmierern Kernfunktionen zur Verfügung, z. B. Sammlungen, Ein-/Ausgabe, Textverarbeitung, XML/JSON-Verarbeitung, Netzwerke, Verschlüsselung, Interop, Parallelität und parallele Programmierung.

Eine BCL implementiert auch Typen, die die Sprache C# selbst benötigt (für Funktionen wie Aufzählung, Abfragen und Asynchronität) und ermöglicht dir den expliziten Zugriff auf Funktionen der CLR, wie Reflection und Speicherverwaltung.

Laufzeiten

Eine Runtime (auch Framework genannt) ist eine einsatzfähige Einheit, die du herunterlädst und installierst. Eine Runtime besteht aus einer CLR (mit ihrer BCL) und einer optionalen Anwendungsschicht, die für die Art der Anwendung, die du schreibst, spezifisch ist - Web, Mobile, Rich Client usw. (Wenn du eine Kommandozeilen-Konsolenanwendung oder eine Nicht-UI-Bibliothek schreibst, brauchst du keine Anwendungsschicht).

Wenn du eine Anwendung schreibst, wählst du eine bestimmte Laufzeit aus. Das bedeutet, dass deine Anwendung die Funktionen nutzt, die die Laufzeit zur Verfügung stellt, und dass sie davon abhängt. Die Wahl der Laufzeit bestimmt auch, welche Plattformen deine Anwendung unterstützen wird.

In der folgenden Tabelle sind die wichtigsten Laufzeitoptionen aufgeführt:

Anwendungsschicht CLR/BCL Programmtyp Läuft auf...
ASP.NET .NET 8 Web Windows, Linux, macOS
Windows-Desktop .NET 8 Windows Windows 10+
WinUI 3 .NET 8 Windows Windows 10+
MAUI .NET 8 Mobil, Desktop iOS, Android, macOS, Windows 10+
.NET Framework .NET Framework Web, Windows Windows 7+

Abbildung 1-2 zeigt diese Informationen grafisch und dient auch als Leitfaden für die Inhalte des Buches.

Runtimes for C#
Abbildung 1-2. Laufzeiten für C#

.NET 8

.NET 8 ist das Flaggschiff der Open-Source-Laufzeitumgebung von Microsoft. Du kannst Web- und Konsolenanwendungen schreiben, die unter Windows, Linux und macOS laufen; Rich-Client-Anwendungen, die unter Windows 10+ und macOS laufen; und mobile Apps, die unter iOS und Android laufen. Dieses Buch konzentriert sich auf die .NET 8 CLR und BCL.

Anders als .NET Framework ist .NET 8 auf Windows-Rechnern nicht vorinstalliert. Wenn du versuchst, eine .NET 8-Anwendung auszuführen, ohne dass die richtige Runtime vorhanden ist, wird eine Meldung angezeigt, die dich auf eine Webseite verweist, von der du die Runtime herunterladen kannst. Du kannst dies vermeiden, indem du eine eigenständige Bereitstellung erstellst, die die für die Anwendung erforderlichen Teile der Laufzeitumgebung enthält.

Hinweis

Die Update-Historie von .NET sieht folgendermaßen aus: .NET Core 1.x → .NET Core 2.x →.NET Core 3.x → .NET 5 → .NET 6 → .NET 7 → .NET 8. Nach .NET Core 3 hat Microsoft das Wort "Core" aus dem Namen gestrichen und die Version 4 übersprungen, um Verwechslungen mit dem .NET Framework 4.x zu vermeiden, das allen vorangegangenen Runtimes vorausging, aber immer noch unterstützt wird und weit verbreitet ist.

Das bedeutet, dass Assemblies, die unter .NET Core 1.x → .NET 7 kompiliert wurden, in den meisten Fällen ohne Änderungen unter .NET 8 laufen. Im Gegensatz dazu sind Assemblies, die unter (einer beliebigen Version von) .NET Framework kompiliert wurden, normalerweise nicht mit .NET 8 kompatibel.

Windows Desktop und WinUI 3

Für das Schreiben von Rich-Client-Anwendungen, die unter Windows 10 und höher laufen, kannst du zwischen den klassischen Windows Desktop-APIs (Windows Forms und WPF) und WinUI 3 wählen. Die Windows Desktop-APIs sind Teil der .NET Desktop-Runtime, während WinUI 3 Teil des Windows App SDK ist (ein separater Download).

Die klassischen Windows-Desktop-APIs gibt es seit 2006. Sie werden von Drittanbieter-Bibliotheken hervorragend unterstützt und bieten eine Fülle von beantworteten Fragen auf Seiten wie StackOverflow. WinUI 3 wurde 2022 veröffentlicht und ist für das Schreiben moderner immersiver Anwendungen gedacht, die die neuesten Windows 10+ Steuerelemente enthalten. Sie ist ein Nachfolger der Universal Windows Platform (UWP).

MAUI

MAUI (Multi-platform App UI) wurde in erster Linie für die Erstellung von mobilen Apps für iOS und Android entwickelt, kann aber auch für Desktop-Apps verwendet werden, die über Mac Catalyst und WinUI 3 auf macOS und Windows laufen. MAUI ist eine Weiterentwicklung von Xamarin und ermöglicht es, mit einem einzigen Projekt mehrere Plattformen zu bedienen.

Hinweis

Für plattformübergreifende Desktop-Anwendungen bietet eine Drittanbieter-Bibliothek namens Avalonia eine Alternative zu MAUI. Avalonia läuft auch unter Linux und ist architektonisch einfacher als MAUI (da sie ohne die Catalyst/WinUI-Indirektionsschicht arbeitet). Avalonia verfügt über eine WPF-ähnliche API und bietet außerdem ein kommerzielles Add-on namens XPF, das nahezu vollständige WPF-Kompatibilität bietet.

.NET Framework

.NET Framework ist Microsofts ursprüngliche Windows-Laufzeitumgebung zum Schreiben von Web- und Rich-Client-Anwendungen, die (nur) auf dem Windows-Desktop/Server laufen. Es sind keine größeren neuen Versionen geplant, obwohl Microsoft die aktuelle Version 4.8 aufgrund der Fülle der bestehenden Anwendungen weiterhin unterstützen und pflegen wird.

Mit dem .NET Framework wird die CLR/BCL in die Anwendungsschicht integriert. Anwendungen, die mit .NET Framework geschrieben wurden, können unter .NET 8 neu kompiliert werden, obwohl sie in der Regel einige Änderungen erfordern. Einige Funktionen von .NET Framework sind in .NET 8 nicht vorhanden (und umgekehrt).

Das .NET Framework ist bei Windows vorinstalliert und wird automatisch über Windows Update gepatcht. Wenn du .NET Framework 4.8 verwendest, kannst du die Funktionen von C# 7.3 und früher nutzen. (Du kannst dies umgehen, indem du in der Projektdatei eine neuere Sprachversion angibst. Dadurch werden alle aktuellen Sprachfunktionen freigeschaltet, mit Ausnahme derer, die von einer neueren Laufzeitumgebung unterstützt werden).

Hinweis

Das Wort ".NET" wird seit langem als Oberbegriff für alle Technologien verwendet, die das Wort ".NET" enthalten (.NET Framework, .NET Core, .NET Standard usw.).

Das bedeutet, dass Microsofts Umbenennung von .NET Core in .NET eine unglückliche Zweideutigkeit geschaffen hat. In diesem Buch werden wir das neue .NET als .NET 5+ bezeichnen, wenn es zu Unklarheiten kommt. Und um auf .NET Core und seine Nachfolger zu verweisen, werden wir den Ausdruck ".NET Core und .NET 5+" verwenden.

Um die Verwirrung noch zu vergrößern, ist .NET (5+) ein Framework, das sich jedoch stark vom .NET Framework unterscheidet. Daher verwenden wir, wenn möglich, den Begriff Runtime anstelle von Framework.

Nischen-Laufzeiten

Außerdem gibt es die folgenden Nischenlaufzeiten:

  • Unity ist eine Spieleentwicklungsplattform, die es ermöglicht, Spiellogik mit C# zu skripten.

  • DieUniversal Windows Platform (UWP) wurde für das Schreiben von Touch-first-Anwendungen entwickelt, die auf Windows 10+ Desktop und Geräten, einschließlich Xbox, Surface Hub und HoloLens, laufen. UWP-Apps sind sandboxed und werden über den Windows Store ausgeliefert. UWP nutzt eine Version der .NET Core 2.2 CLR/BCL, und es ist unwahrscheinlich, dass diese Abhängigkeit aktualisiert wird. Stattdessen hat Microsoft empfohlen, auf den modernen Ersatz WinUI 3 umzusteigen. Da WinUI 3 aber nur den Windows-Desktop unterstützt, ist UWP immer noch eine Nischenanwendung für Xbox, Surface Hub und HoloLens.

  • Das .NET Micro Framework ist für die Ausführung von .NET-Code auf stark ressourcenbeschränkten eingebetteten Geräten (unter einem Megabyte) gedacht.

Es ist auch möglich, verwalteten Code in SQL Server auszuführen. Mit der SQL Server CLR-Integration kannst du benutzerdefinierte Funktionen, Stored Procedures und Aggregationen in C# schreiben und sie dann von SQL aus aufrufen. Dies funktioniert in Verbindung mit dem .NET Framework und einer speziellen "gehosteten" CLR, die eine Sandbox erzwingt, um die Integrität des SQL Server-Prozesses zu schützen.

Eine kurze Geschichte von C#

Im Folgenden findest du eine umgekehrte Chronologie der neuen Funktionen in jeder C#-Version, damit auch Leser, die bereits mit einer älteren Version der Sprache vertraut sind, davon profitieren können.

Was ist neu in C# 12

C# 12 ist im Lieferumfang von Visual Studio 2022 enthalten und wird verwendet, wenn du .NET 8 anvisierst.

Sammlung Ausdrücke

Anstatt ein Array wie folgt zu initialisieren:

char[] vowels = {'a','e','i','o','u'};

kannst du jetzt eckige Klammern verwenden (ein Sammelausdruck):

char[] vowels = ['a','e','i','o','u'];

Sammlungsausdrücke haben zwei große Vorteile. Erstens funktioniert dieselbe Syntax auch mit anderen Sammlungstypen wie Listen und Sets (und sogar den Low-Level-Span-Typen):

List<char> list         = ['a','e','i','o','u'];
HashSet<char> set       = ['a','e','i','o','u'];
ReadOnlySpan<char> span = ['a','e','i','o','u'];

Zweitens sind sie target-typed, was bedeutet, dass du den Typ in anderen Szenarien, in denen der Compiler ihn ableiten kann, weglassen kannst, zum Beispiel beim Aufruf von Methoden:

Foo (['a','e','i','o','u']);

void Foo (char[] letters) { ... }

Siehe "Sammlungsinitialisierer und Sammlungsausdrücke" für weitere Details.

Primäre Konstruktoren in Klassen und Structs

Ab C# 12 kannst du eine Parameterliste direkt nach einer Klassendeklaration (oder struct) einfügen:

class Person (string firstName, string lastName)
{
  public void Print() => Console.WriteLine (firstName + " " + lastName);
}

Dies weist den Compiler an, automatisch einen primären Konstruktor zu erstellen, der Folgendes ermöglicht:

Person p = new Person ("Alice", "Jones");
p.Print();    // Alice Jones

Diese Funktion gibt es seit C# 9 mit Datensätzen, wobei sie sich etwas anders verhalten. Bei Datensätzen erzeugt der Compiler (standardmäßig) eine öffentliche init-only-Eigenschaft für jeden primären Konstruktorparameter. Das ist bei Klassen und Strukturen nicht der Fall; um das gleiche Ergebnis zu erzielen, musst du diese Eigenschaften explizit definieren:

class Person (string firstName, string lastName)
{
  public string FirstName { get; set; } = firstName;
  public string LastName { get; set; } = lastName;
}

Primäre Konstruktoren funktionieren gut in einfachen Szenarien. Wir beschreiben ihre Feinheiten und Grenzen in "Primäre Konstruktoren (C# 12)".

Standard-Lambdaparameter

Genauso wie normale Methoden können auch Parameter mit Standardwerten definiert werden:

void Print (string message = "") => Console.WriteLine (message);

so können auch Lambda-Ausdrücke:

var print = (string message = "") => Console.WriteLine (message);

print ("Hello");
print ();

Diese Funktion ist bei Bibliotheken wie der ASP.NET Minimal API nützlich.

Alias jeder Typ

In C# war es schon immer möglich, einen einfachen oder generischen Typ über die Direktive using zu aliasieren:

using ListOfInt = System.Collections.Generic.List<int>;

var list = new ListOfInt();

Ab C# 12 funktioniert dieser Ansatz auch mit anderen Arten von Typen wie Arrays und Tupeln:

using NumberList = double[];
using Point = (int X, int Y);

NumberList numbers = { 2.5, 3.5 };
Point p = (3, 4);

Andere neue Funktionen

C# 12 unterstützt über das Attribut [System.Runtime.CompilerServices.InlineArray] auch Inline-Arrays. Dies ermöglicht die Erstellung von Arrays fester Größe in einer Struktur, ohne dass ein unsicherer Kontext erforderlich ist, und ist in erster Linie für die Verwendung innerhalb der Laufzeit-APIs gedacht.

Was ist neu in C# 11?

C# 11 wird mit Visual Studio 2022 ausgeliefert und wird standardmäßig verwendet, wenn du .NET 7 anvisierst.

Rohe String-Literale

Wenn du eine Zeichenkette in drei oder mehr Anführungszeichen einschließt, entsteht ein rohes String-Literal, das fast jede beliebige Zeichenfolge enthalten kann, ohne dass sie escaped oder verdoppelt wird. Das macht es einfach, JSON-, XML- und HTML-Literale sowie reguläre Ausdrücke und Quellcode darzustellen:

string raw = """<file path="c:\temp\test.txt"></file>""";

Raw-String-Literale können mehrzeilig sein und erlauben eine Interpolation über das Präfix $:

string multiLineRaw = $"""
  Line 1
  Line 2
  The date and time is {DateTime.Now}
  """;

Die Verwendung von zwei (oder mehr) $ Zeichen in einem rohen String-Literal-Präfix ändert die Interpolationssequenz von einer Klammer in zwei (oder mehr) Klammern, wodurch du Klammern in den String selbst einfügen kannst:

Console.WriteLine ($$"""{ "TimeStamp": "{{DateTime.Now}}" }""");
// Output: { "TimeStamp": "01/01/2024 12:13:25 PM" }

Wir behandeln die Feinheiten dieser Funktion in "Raw string literals (C# 11)" und "String interpolation".

UTF-8 Zeichenketten

Mit dem Suffix u8 erstellst du String-Literale, die in UTF-8 und nicht in UTF-16 kodiert sind. Diese Funktion ist für fortgeschrittene Szenarien gedacht, wie z. B. die Low-Level-Behandlung von JSON-Text in Performance-Hotspots:

ReadOnlySpan<byte> utf8 = "ab→cd"u8;  // Arrow symbol consumes 3 bytes
Console.WriteLine (utf8.Length);      // 7

Der zugrundeliegende Typ ist ReadOnlySpan<byte> (Kapitel 23), den du in ein Byte-Array umwandeln kannst, indem du seine Methode ToArray() aufrufst.

Muster auflisten

Listenmuster entsprechen einer Reihe von Elementen in eckigen Klammern und funktionieren mit jedem Auflistungstyp, der abzählbar (mit einer Count oder Length Eigenschaft) und indizierbar ist (mit einem Indexer vom Typ int oder System.Index):

int[] numbers = { 0, 1, 2, 3, 4 };
Console.WriteLine (numbers is [0, 1, 2, 3, 4]);   // True

Ein Unterstrich entspricht einem einzelnen Element eines beliebigen Wertes und zwei Punkte entsprechen null oder mehr Elementen (einem Slice):

Console.WriteLine (numbers is [_, 1, .., 4]);     // True

Einem Slice kann das Muster var folgen - siehe "Listenmuster" für weitere Informationen.

Erforderliche Mitglieder

Die Anwendung des required Modifizierers auf ein Feld oder eine Eigenschaft zwingt die Verbraucher dieser Klasse oder Struktur, dieses Mitglied über einen Objektinitialisierer zu füllen, wenn es konstruiert wird:

Asset a1 = new Asset { Name = "House" };  // OK
Asset a2 = new Asset();                   // Error: will not compile!

class Asset { public required string Name; }

Mit dieser Funktion kannst du das Schreiben von Konstruktoren mit langen Parameterlisten vermeiden, was die Unterklassenbildung vereinfachen kann. Solltest du einen Konstruktor schreiben wollen, kannst du das Attribut [SetsRequiredMembers] verwenden, um die Beschränkung der erforderlichen Mitglieder für diesen Konstruktor zu umgehen - siehe "Erforderliche Mitglieder (C# 11)" für weitere Informationen.

Statische virtuelle/abstrakte Schnittstellenmitglieder

Ab C# 11 können Interfaces Member als static virtual oder static abstract deklarieren:

public interface IParsable<TSelf>
{
   static abstract TSelf Parse (string s);
}

Diese Mitglieder sind als statische Funktionen in Klassen oder Structs implementiert und können polymorph über einen eingeschränkten Typparameter aufgerufen werden:

T ParseAny<T> (string s) where T : IParsable<T> => T.Parse (s);

Operatorfunktionen können auch als static virtual oder static abstract deklariert werden.

Einzelheiten dazu findest du unter "Statische virtuelle/abstrakte Schnittstellenmitglieder" und "Statischer Polymorphismus". Wir beschreiben auch, wie man statische abstrakte Mitglieder über Reflexion aufruft, unter "Aufrufen statischer virtueller/abstrakter Schnittstellenmitglieder".

Generische Mathe

Die Schnittstelle System.Numerics.INumber<TSelf> (neu in .NET 7) vereinheitlicht arithmetische Operationen für alle numerischen Typen, so dass generische Methoden wie die folgende geschrieben werden können:

T Sum<T> (T[] numbers) where T : INumber<T>
{
  T total = T.Zero;
  foreach (T n in numbers)
    total += n;      // Invokes addition operator for any numeric type
  return total;
}

int intSum = Sum (3, 5, 7);
double doubleSum = Sum (3.2, 5.3, 7.1);
decimal decimalSum = Sum (3.2m, 5.3m, 7.1m);

INumber<TSelf> wird von allen reellen und ganzzahligen numerischen Typen in .NET (sowie von char) implementiert und umfasst mehrere Schnittstellen, die statische abstrakte Operatordefinitionen wie die folgenden enthalten:

static abstract TResult operator + (TSelf left, TOther right);

Wir behandeln dies in "Polymorphe Operatoren" und "Generische Mathematik".

Andere neue Funktionen

Auf einen Typ mit dem Modifikator file kann nur innerhalb derselben Datei zugegriffen werden und ist für die Verwendung innerhalb von Quelltextgeneratoren vorgesehen:

file class Foo { ... }

Mit C# 11 wurden auch geprüfte Operatoren eingeführt (siehe "Geprüfte Operatoren"), um Operatorfunktionen zu definieren, die innerhalb von checked Blöcken aufgerufen werden (dies war für eine vollständige Implementierung der generischen Mathematik erforderlich). Mit C# 11 wurde auch die Anforderung gelockert, dass jedes Feld im Konstruktor einer Struktur ausgefüllt werden muss (siehe "Struct Construction Semantics").

Schließlich wurden die Typen nint und nuint für Ganzzahlen in nativer Größe, die in C# 9 eingeführt wurden, um dem Adressraum des Prozesses zur Laufzeit (32 oder 64 Bit) zu entsprechen, in C# 11 verbessert, wenn es um .NET 7 oder später geht. Insbesondere wurde die Unterscheidung zwischen diesen Typen zur Kompilierzeit und den ihnen zugrunde liegenden Laufzeittypen (IntPtr und UIntPtr) aufgehoben, wenn .NET 7+ verwendet wird. Siehe "Native-Sized Integers" für eine ausführliche Diskussion.

Was ist neu in C# 10?

C# 10 wird mit Visual Studio 2022 ausgeliefert und wird verwendet, wenn du auf .NET 6 abzielst.

Namensräume mit Dateischnittstelle

In dem häufigen Fall, dass alle Typen in einer Datei in einem einzigen Namensraum definiert sind, reduziert eine dateiübergreifende Namensraumdeklaration in C# 10 das Durcheinander und eliminiert eine unnötige Einrückungsebene:

namespace MyNamespace;  // Applies to everything that follows in the file.

class Class1 {}         // inside MyNamespace
class Class2 {}         // inside MyNamespace

Die globale using-Direktive

Wenn du einer using Direktive das Schlüsselwort global voranstellst, gilt die Direktive für alle Dateien im Projekt:

global using System;
global using System.Collection.Generic;

So vermeidest du, dass du in jeder Datei dieselben Direktiven wiederholst. global using Direktiven funktionieren mit using static.

Außerdem unterstützen .NET 6-Projekte jetzt implizite globale using-Direktiven: Wenn das Element ImplicitUsings in der Projektdatei auf true gesetzt ist, werden die am häufigsten verwendeten Namensräume automatisch importiert (basierend auf dem SDK-Projekttyp). Siehe "Die globale using-Direktive" für weitere Details.

Nicht-destruktive Mutation für anonyme Typen

In C# 9 wurde das Schlüsselwort with eingeführt, um eine nicht-destruktive Mutation von Datensätzen durchzuführen. In C# 10 funktioniert das Schlüsselwort with auch mit anonymen Typen:

var a1 = new { A = 1, B = 2, C = 3, D = 4, E = 5 };
var a2 = a1 with { E = 10 }; 
Console.WriteLine (a2);      // { A = 1, B = 2, C = 3, D = 4, E = 10 }

Neue Dekonstruktionssyntax

Mit C# 7 wurde die Dekonstruktionssyntax für Tupel (oder jeden Typ mit einer Deconstruct Methode). C# 10 geht noch einen Schritt weiter und erlaubt es dir, Zuweisung und Deklaration in derselben Dekonstruktion zu kombinieren:

var point = (3, 4);
double x = 0;
(x, double y) = point;

Feldinitialisierungen und parameterlose Konstruktoren in structs

Ab C# 10 kannst du Feldinitialisierungen und parameterlose Konstruktoren in Structs einfügen (siehe "Structs"). Diese werden nur ausgeführt, wenn der Konstruktor explizit aufgerufen wird, und können daher leicht umgangen werden - zum Beispiel mit dem Schlüsselwort default. Diese Funktion wurde vor allem zum Vorteil von struct-Records eingeführt.

Datensatz-Strukturen

Datensätze wurden erstmals in C# 9 eingeführt, wo sie als kompilierte, erweiterte Klasse fungierten. In C# 10 können Datensätze auch Structs sein:

record struct Point (int X, int Y);

Die Regeln sind ansonsten ähnlich: Record-Strukturen haben fast die gleichen Eigenschaften wie Klassen-Strukturen (siehe "Records"). Eine Ausnahme ist, dass die vom Compiler erzeugten Eigenschaften von Record-Strukturen beschreibbar sind, es sei denn, du stellst der Record-Deklaration das Schlüsselwort readonly voran.

Erweiterungen des Lambda-Ausdrucks

Die Syntax von Lambda-Ausdrücken wurde in mehrfacher Hinsicht verbessert. Erstens ist die implizite Typisierung (var) erlaubt:

var greeter = () => "Hello, world";

Der implizite Typ für einen Lambda-Ausdruck ist ein Action oder Func Delegierter, also ist greeter in diesem Fall vom Typ Func<string>. Du musst alle Parametertypen explizit angeben:

var square = (int x) => x * x;

Zweitens kann ein Lambda-Ausdruck einen Rückgabetyp angeben:

var sqr = int (int x) => x;

Dies dient in erster Linie dazu, die Compilerleistung bei komplexen verschachtelten Lambdas zu verbessern.

Drittens kannst du einen Lambda-Ausdruck an einen Methodenparameter vom Typ object, Delegate, oder Expression übergeben:

M1 (() => "test");   // Implicitly typed to Func<string>
M2 (() => "test");   // Implicitly typed to Func<string>
M3 (() => "test");   // Implicitly typed to Expression<Func<string>>

void M1 (object x) {}
void M2 (Delegate x) {}
void M3 (Expression x) {}

Schließlich kannst du Attribute auf die durch Kompilierung erzeugte Zielmethode eines Lambda-Ausdrucks (sowie auf seine Parameter und seinen Rückgabewert) anwenden:

Action a = [Description("test")] () => { };

Siehe "Attribute auf Lambda-Ausdrücke anwenden" für weitere Details.

Verschachtelte Eigenschaftsmuster

Die folgende vereinfachte Syntax ist in C# 10 für den Abgleich verschachtelter Eigenschaftsmuster zulässig (siehe "Eigenschaftsmuster"):

var obj = new Uri ("https://www.linqpad.net");
if (obj is Uri { Scheme.Length: 5 }) ...

Dies ist gleichbedeutend mit:

if (obj is Uri { Scheme: { Length: 5 }}) ...

CallerArgumentExpression

Ein Methodenparameter, auf den du das Attribut [CallerArgumentExpression] anwendest, fängt einen Argumentausdruck von der Aufrufseite ein:

Print (Math.PI * 2);

void Print (double number,
           [CallerArgumentExpression("number")] string expr = null)
  => Console.WriteLine (expr);

// Output: Math.PI * 2

Diese Funktion ist vor allem für Validierungs- und Assertion-Bibliotheken gedacht (siehe "CallerArgumentExpression").

Andere neue Funktionen

Die #line Direktive wurde in C# 10 erweitert, damit eine Spalte und ein Bereich angegeben werden können.

Interpolierte Strings in C# 10 können Konstanten sein, solange die interpolierten Werte Konstanten sind.

Datensätze können die Methode ToString() in C# 10 versiegeln.

Die definitive Zuweisungsanalyse von C# wurde verbessert, damit Ausdrücke wie der folgende funktionieren:

if (foo?.TryParse ("123", out var number) ?? false)
  Console.WriteLine (number);

(Vor C# 10 hat der Compiler eine Fehlermeldung ausgegeben: "Verwendung einer nicht zugewiesenen lokalen Variable 'number'.")

Was ist neu in C# 9.0

C# 9.0 wird mit Visual Studio 2019 ausgeliefert und wird verwendet, wenn du auf .NET 5 abzielst.

Top-Level-Anweisungen

Mit Top-Level-Anweisungen (siehe "Top-Level-Anweisungen") kannst du ein Programm ohne den Ballast einer Main Methode und Program Klasse schreiben:

using System;
Console.WriteLine ("Hello, world");

Top-Level-Anweisungen können Methoden enthalten (die sich wie lokale Methoden verhalten). Sie können auch über die "magische" Variable args auf Kommandozeilenargumente zugreifen und einen Wert an den Aufrufer zurückgeben. Auf Top-Level-Anweisungen können Typ- und Namensraumdeklarationen folgen.

Init-Only-Setter

Ein Init-Only-Setter (siehe "Init-Only-Setter") in einer Eigenschaftsdeklaration verwendet das Schlüsselwort init anstelle des Schlüsselworts set:

class Foo { public int ID { get; init; } }

Diese Eigenschaft verhält sich wie eine schreibgeschützte Eigenschaft, außer dass sie auch über einen Objektinitialisierer gesetzt werden kann:

var foo = new Foo { ID = 123 };

Dies ermöglicht es, unveränderliche (schreibgeschützte) Typen zu erstellen, die über einen Objektinitialisierer statt über einen Konstruktor gefüllt werden können, und hilft, das Antipattern von Konstruktoren zu vermeiden, die eine große Anzahl optionaler Parameter akzeptieren. Init-Only-Setter ermöglichen außerdem eine nicht-destruktive Mutation, wenn sie in Datensätzen verwendet werden.

Aufzeichnungen

Ein Record (siehe "Records") ist eine besondere Art von Klasse, die für unveränderliche Daten konzipiert ist. Das Besondere an ihr ist, dass sie eine nicht-destruktive Mutation über ein neues Schlüsselwort (with) unterstützt:

Point p1 = new Point (2, 3);
Point p2 = p1 with { Y = 4 };   // p2 is a copy of p1, but with Y set to 4
Console.WriteLine (p2);         // Point { X = 2, Y = 4 }

record Point
{
  public Point (double x, double y) => (X, Y) = (x, y);

  public double X { get; init; }
  public double Y { get; init; }    
}

In einfachen Fällen kann ein Datensatz auch die Definition von Eigenschaften und das Schreiben eines Konstruktors und Dekonstruktors überflüssig machen. Wir können unsere Point Datensatzdefinition durch die folgende ersetzen, ohne dass die Funktionalität darunter leidet:

record Point (double X, double Y);

Wie Tupel haben auch Datensätze standardmäßig strukturelle Gleichheit. Datensätze können Unterklassen anderer Datensätze sein und dieselben Konstrukte enthalten, die auch Klassen enthalten können. Der Compiler implementiert Datensätze zur Laufzeit als Klassen.

Verbesserungen bei der Mustererkennung

Mit dem Beziehungsmuster (siehe "Muster") können die Operatoren <, >, <= und >= in Mustern erscheinen:

string GetWeightCategory (decimal bmi) => bmi switch {
  < 18.5m => "underweight",
  < 25m => "normal",
  < 30m => "overweight",
  _ => "obese" };

Mit Musterkombinatoren kannst du Muster über drei neue Schlüsselwörter (and, or und not) kombinieren:

bool IsVowel (char c) => c is 'a' or 'e' or 'i' or 'o' or 'u';

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

Wie bei den Operatoren && und || hat auch and einen höheren Vorrang als or. Du kannst dies mit Klammern außer Kraft setzen.

Der not Kombinator kann zusammen mit dem Typmuster verwendet werden, um zu prüfen, ob ein Objekt ein Typ ist (oder nicht):

if (obj is not string) ...

Neue Ausdrücke vom Typ "Ziel

Wenn du ein Objekt konstruierst, kannst du in C# 9 den Typnamen weglassen, wenn der Compiler ihn eindeutig ableiten kann:

System.Text.StringBuilder sb1 = new();
System.Text.StringBuilder sb2 = new ("Test");

Das ist besonders nützlich, wenn die Variablendeklaration und die Initialisierung in verschiedenen Teilen deines Codes liegen:

class Foo
{
  System.Text.StringBuilder sb;
  public Foo (string initialValue) => sb = new (initialValue);
}

Und im folgenden Szenario:

MyMethod (new ("test"));
void MyMethod (System.Text.StringBuilder sb) { ... }

Weitere Informationen findest du unter "Neue zielsprachliche Ausdrücke".

Interop-Verbesserungen

C# 9 führt Funktionszeiger ein (siehe "Funktionszeiger" und "Rückrufe mit Funktionszeigern"). Ihr Hauptzweck ist es, nicht verwalteten Code zu ermöglichen, statische Methoden in C# ohne den Overhead einer Delegateninstanz aufzurufen. Dabei kann die P/Invoke-Schicht umgangen werden, wenn die Argumente und Rückgabetypen blittable sind (auf beiden Seiten identisch dargestellt).

C# 9 führt auch die Integer-Typen nint und nuint ein (siehe "Native-Sized Integers"), die zur Laufzeit auf System.IntPtr und System.UIntPtr abgebildet werden. Zur Kompilierzeit verhalten sie sich wie numerische Typen mit Unterstützung für arithmetische Operationen.

Andere neue Funktionen

Außerdem kannst du mit C# 9 jetzt:

  • Überschreibe eine Methode oder schreibgeschützte Eigenschaft so, dass sie einen abgeleiteten Typ zurückgibt (siehe "Kovariante Rückgabetypen").

  • Wende Attribute auf lokale Funktionen an (siehe "Attribute").

  • Wende das Schlüsselwort static auf Lambda-Ausdrücke oder lokale Funktionen an, um sicherzustellen, dass du nicht versehentlich lokale oder Instanzvariablen erfasst (siehe "Statische Lambdas").

  • Du kannst jeden Typ mit der foreach Anweisung verwenden, indem du eine GetEnumerator Erweiterungsmethode schreibst.

  • Definiere eine Modulinitialisierungsmethode, die einmal ausgeführt wird, wenn eine Assembly zum ersten Mal geladen wird, indem du das Attribut [ModuleInitializer] auf eine (static void parameterless) Methode anwendest.

  • Verwirf ein "discard" (Unterstrich) als Argument für einen Lambda-Ausdruck.

  • Schreibe erweiterte partielle Methoden, die zwingend erforderlich sind, um Szenarien wie die neuen Quellgeneratoren von Roslyn zu implementieren (siehe "Erweiterte partielle Methoden").

  • Wende ein Attribut auf Methoden, Typen oder Module an, um zu verhindern, dass lokale Variablen von der Laufzeit initialisiert werden (siehe "[SkipLocalsInit]").

Was ist neu in C# 8.0

C# 8.0 wurde erstmals mit Visual Studio 2019 ausgeliefert und wird auch heute noch verwendet, wenn du .NET Core 3 oder .NET Standard 2.1 anvisierst.

Indizes und Bereiche

Indizes und Bereiche vereinfachen die Arbeit mit Elementen oder Teilen eines Arrays (oder den Low-Level-Typen Span<T> und ReadOnlySpan<T>).

Mit Indizes kannst du dich auf Elemente relativ zum Ende eines Arrays beziehen, indem du den ^ Operator verwendest. ^1 bezieht sich auf das letzte Element, ^2 auf das vorletzte Element und so weiter:

char[] vowels = new char[] {'a','e','i','o','u'};
char lastElement  = vowels [^1];   // 'u'
char secondToLast = vowels [^2];   // 'o'

Mit Ranges kannst du ein Array mit Hilfe des .. Operators "zerschneiden":

char[] firstTwo =  vowels [..2];    // 'a', 'e'
char[] lastThree = vowels [2..];    // 'i', 'o', 'u'
char[] middleOne = vowels [2..3]    // 'i'
char[] lastTwo =   vowels [^2..];   // 'o', 'u'

C# implementiert Indizes und Bereiche mit Hilfe der Typen Index und Range:

Index last = ^1;
Range firstTwoRange = 0..2;
char[] firstTwo = vowels [firstTwoRange];   // 'a', 'e'

Du kannst Indizes und Bereiche in deinen eigenen Klassen unterstützen, indem du einen Indexer mit einem Parametertyp von Index oder Range definierst:

class Sentence
{
  string[] words = "The quick brown fox".Split();

  public string this   [Index index] => words [index];
  public string[] this [Range range] => words [range];
}

Weitere Informationen findest du unter "Indizes und Bandbreiten".

Null-Koaleszenz-Zuweisung

Der ??= Operator weist eine Variable nur zu, wenn sie null ist. Anstelle von

if (s == null) s = "Hello, world";

kannst du das jetzt schreiben:

s ??= "Hello, world";

Deklarationen verwenden

Wenn du die Klammern und den Anweisungsblock nach einer using Anweisung weglässt, wird sie zu einer using-Deklaration. Die Ressource wird dann entsorgt, wenn die Ausführung außerhalb des einschließenden Anweisungsblocks erfolgt:

if (File.Exists ("file.txt"))
{
  using var reader = File.OpenText ("file.txt");
  Console.WriteLine (reader.ReadLine());
  ...
}

In diesem Fall wird reader entsorgt, wenn die Ausführung außerhalb des if Anweisungsblocks erfolgt.

Schreibgeschützte Mitglieder

In C# 8 kannst du den readonly Modifikator auf die Funktionen einer Struktur anwenden, um sicherzustellen, dass ein Kompilierfehler erzeugt wird, wenn die Funktion versucht, ein Feld zu ändern:

struct Point
{
  public int X, Y;
  public readonly void ResetX() => X = 0;  // Error!
}

Wenn eine readonly Funktion eine nichtreadonly Funktion aufruft, erzeugt der Compiler eine Warnung (und kopiert defensiv die Struktur, um die Möglichkeit einer Mutation zu vermeiden).

Statische lokale Methoden

Wenn du einer lokalen Methode den Modifikator static hinzufügst, kann sie die lokalen Variablen und Parameter der umschließenden Methode nicht sehen. Dies trägt dazu bei, die Kopplung zu verringern, und ermöglicht es der lokalen Methode, Variablen nach Belieben zu deklarieren, ohne Gefahr zu laufen, mit den Variablen der einschließenden Methode zu kollidieren.

Standardschnittstellenmitglieder

In C# 8 kannst du eine Standardimplementierung zu einem Schnittstellenmitglied hinzufügen, so dass es optional implementiert werden kann:

interface ILogger
{
  void Log (string text) => Console.WriteLine (text);
}

Das bedeutet, dass du ein Mitglied zu einer Schnittstelle hinzufügen kannst, ohne Implementierungen zu zerstören. Standardimplementierungen müssen explizit über die Schnittstelle aufgerufen werden:

((ILogger)new Logger()).Log ("message");

Interfaces können auch statische Mitglieder (einschließlich Felder) definieren, auf die von Code innerhalb von Standardimplementierungen zugegriffen werden kann:

interface ILogger
{
  void Log (string text) => Console.WriteLine (Prefix + text);
  static string Prefix = ""; 
}

Oder von außerhalb der Schnittstelle, es sei denn, die Erreichbarkeit wird durch einen Modifikator für das statische Schnittstellenmitglied eingeschränkt (z. B. private, protected oder internal):

ILogger.Prefix = "File log: ";

Instanzfelder sind verboten. Weitere Informationen findest du unter "Standardschnittstellenmitglieder".

Ausdrücke wechseln

Ab C# 8 kannst du switch im Kontext eines Ausdrucks verwenden:

string cardName = cardNumber switch    // assuming cardNumber is an int
{
  13 => "King",
  12 => "Queen",
  11 => "Jack",
  _ => "Pip card"   // equivalent to 'default'
};

Weitere Beispiele findest du unter "Ausdrücke wechseln".

Tupel-, Positions- und Eigenschaftsmuster

C# 8 unterstützt drei neue Muster, die vor allem für Switch-Anweisungen/-ausdrücke von Vorteil sind (siehe "Muster"). Mit Tupel-Mustern kannst du auf mehrere Werte umschalten:

int cardNumber = 12; string suite = "spades";
string cardName = (cardNumber, suite) switch
{
  (13, "spades") => "King of spades",
  (13, "clubs") => "King of clubs",
  ...
};

Positionsmuster ermöglichen eine ähnliche Syntax für Objekte, die einen Dekonstruktor aufweisen, und Eigenschaftsmuster ermöglichen eine Übereinstimmung mit den Eigenschaften eines Objekts. Du kannst alle Muster sowohl in Schaltern als auch mit dem Operator is verwenden. Im folgenden Beispiel wird ein Eigenschaftsmuster verwendet, um zu prüfen, ob obj eine Zeichenkette mit der Länge 4 ist:

if (obj is string { Length:4 }) ...

Nullbare Referenztypen

Während nullbare Werttypen Werttypen nullbar machen, bewirken nullbare Referenztypen das Gegenteil und machen Referenztypen (bis zu einem gewissen Grad) nicht-nullbar, um NullReferenceExceptionzu vermeiden. Nullbare Referenztypen führen eine Sicherheitsebene ein, die ausschließlich vom Compiler in Form von Warnungen oder Fehlern durchgesetzt wird, wenn er Code entdeckt, der Gefahr läuft, eine NullReferenceException zu erzeugen.

Nullbare Referenztypen können entweder auf Projektebene (über das Element Nullable in der Projektdatei .csproj ) oder im Code (über die Direktive #nullable ) aktiviert werden. Nach der Aktivierung macht der Compiler die Nichtnullbarkeit zum Standard: Wenn du willst, dass ein Referenztyp Nullen akzeptiert, musst du das Suffix ? hinzufügen, um einen nullbaren Referenztyp anzugeben:

#nullable enable    // Enable nullable reference types from this point on

string s1 = null;   // Generates a compiler warning! (s1 is non-nullable)
string? s2 = null;  // OK: s2 is nullable reference type

Uninitialisierte Felder erzeugen ebenfalls eine Warnung (wenn der Typ nicht als nullable markiert ist), ebenso wie die Dereferenzierung eines nullable Referenztyps, wenn der Compiler denkt, dass eine NullReferenceException auftreten könnte:

void Foo (string? s) => Console.Write (s.Length);  // Warning (.Length)

Um die Warnung zu entfernen, kannst du den Null-Forgiving-Operator (!) verwenden:

void Foo (string? s) => Console.Write (s!.Length);

Eine ausführliche Diskussion findest du unter "Nullable Reference Types".

Asynchrone Ströme

Vor C# 8 konntest du yield return verwenden, um einen Iterator zu schreiben, oder await, um eine asynchrone Funktion zu schreiben. Aber du konntest nicht beides tun und einen Iterator schreiben, der auf Elemente wartet und diese asynchron ausgibt. C# 8 behebt dieses Problem durch die Einführung von asynchronen Streams:

async IAsyncEnumerable<int> RangeAsync (
  int start, int count, int delay)
{
  for (int i = start; i < start + count; i++)
  {
    await Task.Delay (delay);
    yield return i;
  }
}

Die Anweisung await foreach konsumiert einen asynchronen Stream:

await foreach (var number in RangeAsync (0, 10, 100))
  Console.WriteLine (number);

Weitere Informationen findest du unter "Asynchrone Streams".

Was ist neu in C# 7.x

C# 7.x wurde erstmals mit Visual Studio 2017 ausgeliefert. C# 7.3 wird auch heute noch von Visual Studio 2019 verwendet, wenn du .NET Core 2, .NET Framework 4.6 bis 4.8 oder .NET Standard 2.0 anvisierst.

C# 7.3

Mit C# 7.3 wurden kleinere Verbesserungen an bestehenden Funktionen vorgenommen, wie z.B. die Verwendung von Gleichheitsoperatoren mit Tupeln, eine verbesserte Auflösung von Überladungen und die Möglichkeit, Attribute auf die Hintergrundfelder von automatischen Eigenschaften anzuwenden:

[field:NonSerialized]
public int MyProperty { get; set; }

C# 7.3 baute auch auf den fortschrittlichen Funktionen von C# 7.2 für die Programmierung mit geringer Belegung auf, wie z.B. der Möglichkeit, ref locals neu zuzuweisen, dem Verzicht auf Pin beim Indizieren von fixed Feldern und der Unterstützung von Feldinitialisierungen mit stackalloc:

int* pointer  = stackalloc int[] {1, 2, 3};
Span<int> arr = stackalloc []    {1, 2, 3};

Beachte, dass Stack-Speicher direkt einem Span<T> zugewiesen werden kann. Wir beschreiben Spans - und warum du sie benutzen solltest - in Kapitel 23.

C# 7.2

C# 7.2 fügte einen neuen private protected Modifikator (die Schnittmenge von internal und protected), die Möglichkeit, benannte Argumente beim Methodenaufruf durch Positionsargumente zu ersetzen, und readonly structs hinzu. Eine readonly struct erzwingt, dass alle Felder readonly sind, um die Deklaration der Absicht zu erleichtern und dem Compiler mehr Optimierungsmöglichkeiten zu geben:

readonly struct Point
{
  public readonly int X, Y;   // X and Y must be readonly
}

Mit C# 7.2 wurden außerdem spezielle Funktionen hinzugefügt, die bei der Mikrooptimierung und der Programmierung mit geringem Speicherbedarf helfen: siehe "Der in-Modifikator", "Ref Locals", "Ref Returns" und "Ref Structs".

C# 7.1

Ab C# 7.1 kannst du den Typ weglassen, wenn du das Schlüsselwort default verwendest, wenn der Typ abgeleitet werden kann:

decimal number = default;   // number is decimal

Mit C# 7.1 wurden auch die Regeln für Switch-Anweisungen gelockert (so dass du Pattern-Match auf generische Typparameter anwenden kannst), die Methode Main eines Programms kann asynchron sein und die Namen von Tupel-Elementen können abgeleitet werden:

var now = DateTime.Now;
var tuple = (now.Hour, now.Minute, now.Second);

Numerische wörtliche Verbesserungen

Numerische Literale in C# 7 können Unterstriche enthalten, um die Lesbarkeit zu verbessern. Diese werden als Zifferntrennzeichen bezeichnet und vom Compiler ignoriert:

int million = 1_000_000;

Binäre Literale können mit dem Präfix 0b angegeben werden:

var b = 0b1010_1011_1100_1101_1110_1111;

Out-Variablen und Rückwürfe

C# 7 macht es einfacher, Methoden aufzurufen, die out Parameter enthalten. Erstens kannst du jetzt spontan Out-Variablen deklarieren (siehe "Out-Variablen und Verwerfungen"):

bool successful = int.TryParse ("123", out int result);
Console.WriteLine (result);

Und wenn du eine Methode mit mehreren out Parametern aufrufst, kannst du diejenigen, die dich nicht interessieren, mit dem Unterstrich verwerfen:

SomeBigMethod (out _, out _, out _, out int x, out _, out _, out _);
Console.WriteLine (x);

Typmuster und Mustervariablen

Du kannst auch Variablen mit dem is Operator einführen. Diese werden Mustervariablen genannt (siehe "Einführen einer Mustervariable"):

void Foo (object x)
{
  if (x is string s)
    Console.WriteLine (s.Length);
}

Die Anweisung switch unterstützt auch Typmuster, so dass du nicht nur auf Konstanten, sondern auch auf Typen schalten kannst (siehe "Auf Typen schalten"). Du kannst Bedingungen mit einer when Klausel angeben und auch den null Wert einschalten:

switch (x)
{
  case int i:
    Console.WriteLine ("It's an int!");
    break;
  case string s:
    Console.WriteLine (s.Length);    // We can use the s variable
    break;
  case bool b when b == true:        // Matches only when b is true
    Console.WriteLine ("True");
    break;
  case null:
    Console.WriteLine ("Nothing");
    break;
}

Lokale Methoden

Eine lokale Methode ist eine Methode, die innerhalb einer anderen Funktion deklariert wird (siehe "Lokale Methoden"):

void WriteCubes()
{
  Console.WriteLine (Cube (3));
  Console.WriteLine (Cube (4));
  Console.WriteLine (Cube (5));

  int Cube (int value) => value * value * value;
}

Lokale Methoden sind nur für die enthaltende Funktion sichtbar und können lokale Variablen auf die gleiche Weise erfassen wie Lambda-Ausdrücke.

Mehr ausdrucksstarke Mitglieder

Mit C# 6 wurde die "Fat-Arrow"-Syntax für Methoden, schreibgeschützte Eigenschaften, Operatoren und Indexer eingeführt. C# 7 erweitert diese Syntax auf Konstruktoren, Lese-/Schreibeigenschaften und Finalizer:

public class Person
{
  string name;

  public Person (string name) => Name = name;

  public string Name
  {
    get => name;
    set => name = value ?? "";
  }

  ~Person () => Console.WriteLine ("finalize");
}

Dekonstrukteure

C# 7 führt das Dekonstruktor-Muster ein (siehe "Dekonstruktoren"). Während ein Konstruktor normalerweise eine Reihe von Werten (als Parameter) entgegennimmt und sie Feldern zuweist, macht ein Dekonstruktor das Gegenteil und weist Felder wieder einer Reihe von Variablen zu. Wir könnten einen Dekonstruktor für die Klasse Person aus dem vorangegangenen Beispiel wie folgt schreiben (abgesehen von der Ausnahmebehandlung):

public void Deconstruct (out string firstName, out string lastName)
{
  int spacePos = name.IndexOf (' ');
  firstName = name.Substring (0, spacePos);
  lastName = name.Substring (spacePos + 1);
}

Dekonstrukteure werden mit der folgenden speziellen Syntax aufgerufen:

var joe = new Person ("Joe Bloggs");
var (first, last) = joe;          // Deconstruction
Console.WriteLine (first);        // Joe
Console.WriteLine (last);         // Bloggs

Tupel

Die vielleicht bemerkenswerteste Verbesserung in C# 7 ist die explizite Unterstützung von Tupeln (siehe "Tupel"). Tupel bieten eine einfache Möglichkeit, eine Reihe von zusammenhängenden Werten zu speichern:

var bob = ("Bob", 23);
Console.WriteLine (bob.Item1);   // Bob
Console.WriteLine (bob.Item2);   // 23

Die neuen Tupel in C# sind ein syntaktischer Zucker für die Verwendung von System.ValueTuple<…> generic structs. Aber dank der Compiler-Magie können Tupel-Elemente benannt werden:

var tuple = (name:"Bob", age:23);
Console.WriteLine (tuple.name);     // Bob
Console.WriteLine (tuple.age);      // 23

Mit Tupeln können Funktionen mehrere Werte zurückgeben, ohne auf out Parameter oder zusätzliche Typen zurückgreifen zu müssen:

static (int row, int column) GetFilePosition() => (3, 10);

static void Main()
{
  var pos = GetFilePosition();
  Console.WriteLine (pos.row);      // 3
  Console.WriteLine (pos.column);   // 10
}

Tupel unterstützen implizit das Dekonstruktionsmuster, sodass du sie leicht in einzelne Variablen zerlegen kannst:

static void Main()
{
  (int row, int column) = GetFilePosition();   // Creates 2 local variables
  Console.WriteLine (row);      // 3 
  Console.WriteLine (column);   // 10
}

Ausdrücke werfen

Vor C# 7 war throw immer eine Anweisung. Jetzt kann er auch als Ausdruck in Funktionen mit Ausdrucksform erscheinen:

public string Foo() => throw new NotImplementedException();

Ein throw Ausdruck kann auch in einem ternären bedingten Ausdruck vorkommen:

string Capitalize (string value) =>
  value == null ? throw new ArgumentException ("value") :
  value == "" ? "" :
  char.ToUpper (value[0]) + value.Substring (1);

Was ist neu in C# 6.0

C# 6.0, das mit Visual Studio 2015 ausgeliefert wurde, enthält einen Compiler der neuen Generation, der komplett in C# geschrieben wurde. Der neue Compiler, der unter dem Namen "Roslyn" bekannt ist, stellt die gesamte Compiler-Pipeline über Bibliotheken zur Verfügung und ermöglicht so die Codeanalyse von beliebigem Quellcode. Der Compiler selbst ist quelloffen, und der Quellcode ist unter https://github.com/dotnet/roslyn verfügbar .

Darüber hinaus bietet C# 6.0 einige kleinere, aber wichtige Verbesserungen, die vor allem darauf abzielen, den Code zu entschlacken.

Der Null-Bedingungsoperator ("Elvis") (siehe "Null-Operatoren") verhindert, dass vor dem Aufruf einer Methode oder dem Zugriff auf ein Typmitglied explizit auf Null geprüft werden muss. Im folgenden Beispiel wird result als Null ausgewertet, anstatt eine NullReferenceException auszulösen:

System.Text.StringBuilder sb = null;
string result = sb?.ToString();      // result is null

Funktionen in Form von Ausdrücken (siehe "Methoden") ermöglichen es, Methoden, Eigenschaften, Operatoren und Indexer, die einen einzigen Ausdruck bilden, im Stil eines Lambda-Ausdrucks zu schreiben:

public int TimesTwo (int x) => x * 2;
public string SomeProperty => "Property value";

MitEigenschaftsinitialisierern(Kapitel 3) kannst du einer automatischen Eigenschaft einen Anfangswert zuweisen:

public DateTime TimeCreated { get; set; } = DateTime.Now;

Initialisierte Eigenschaften können auch schreibgeschützt sein:

public DateTime TimeCreated { get; } = DateTime.Now;

Schreibgeschützte Eigenschaften können auch im Konstruktor gesetzt werden, was es einfacher macht, unveränderliche (schreibgeschützte) Typen zu erstellen.

Index-Initialisierer(Kapitel 4) ermöglichen die Initialisierung eines jeden Typs, der einen Indexer bereitstellt, in einem einzigen Schritt:

var dict = new Dictionary<int,string>()
{
  [3] = "three",
  [10] = "ten"
};

DieString-Interpolation (siehe "String-Typ") bietet eine prägnante Alternative zu string.Format:

string s = $"It is {DateTime.Now.DayOfWeek} today";

MitAusnahmefiltern (siehe "try-Anweisungen und Ausnahmen") kannst du eine Bedingung auf einen Catch-Block anwenden:

string html;
try
{
  html = await new HttpClient().GetStringAsync ("http://asef");
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
  ...
}

Mit der Direktive using static (siehe "Namensräume") kannst du alle statischen Mitglieder eines Typs importieren, so dass du diese Mitglieder unqualifiziert verwenden kannst:

using static System.Console;
...
WriteLine ("Hello, world");  // WriteLine instead of Console.WriteLine

Der nameof (Kapitel 3) Operator gibt den Namen einer Variablen, eines Typs oder eines anderen Symbols als String zurück. Dadurch wird vermieden, dass der Code unterbrochen wird, wenn du ein Symbol in Visual Studio umbenennst:

int capacity = 123;
string x = nameof (capacity);   // x is "capacity"
string y = nameof (Uri.Host);   // y is "Host"

Und schließlich ist es dir jetzt erlaubt, await innerhalb von catch und finally Blöcken zu verwenden.

Was ist neu in C# 5.0

Die große Neuerung von C# 5.0 war die Unterstützung für asynchrone Funktionen über zwei neue Schlüsselwörter, async und await. Asynchrone Funktionen ermöglichen asynchrone Fortsetzungen, die es einfacher machen, reaktionsschnelle und thread-sichere Rich-Client-Anwendungen zu schreiben. Sie erleichtern auch das Schreiben von hochgradig nebenläufigen und effizienten E/A-gebundenen Anwendungen, die nicht für jede Operation eine Thread-Ressource binden. Wir behandeln asynchrone Funktionen ausführlich in Kapitel 14.

Was ist neu in C# 4.0

Mit C# 4.0 wurden vier wichtige Verbesserungen eingeführt:

Dynamisches Binden (Kapitel 4 und 19) verlagert das Binden - also dasAuflösen von Typen und Membern - von der Kompilierzeit auf die Laufzeit und ist in Szenarien nützlich, die sonst komplizierten Reflection-Code erfordern würden. Dynamisches Binden ist auch bei der Interaktion mit dynamischen Sprachen und COM-Komponenten nützlich.

Mit optionalen Parametern(Kapitel 2) können Funktionen Standardparameterwerte angeben, so dass der Aufrufer Argumente weglassen kann, und mit benannten Argumenten kann ein Funktionsaufrufer ein Argument anhand seines Namens und nicht anhand seiner Position identifizieren.

Die Regeln für dieTypvarianz wurden in C# 4.0 (Kapitel 3 und 4) gelockert, so dass Typparameter in generischen Schnittstellen und generischen Delegaten als kovariant oder kontravariant markiert werden können, was natürlichere Typkonvertierungen ermöglicht.

DieCOM-Interoperabilität(Kapitel 24) wurde in C# 4.0 auf drei Arten verbessert. Erstens können Argumente per Referenz ohne das Schlüsselwort ref übergeben werden (besonders nützlich in Verbindung mit optionalen Parametern). Zweitens können Assemblies, die COM-Interop-Typen enthalten, verlinkt werden, anstatt sie zu referenzieren. Verlinkte Interop-Typen unterstützen die Typäquivalenz, so dass keine primären Interop-Assemblies mehr benötigt werden und die Versionskontrolle und das Deployment kein Problem mehr darstellen. Drittens werden Funktionen, die COM-Variantentypen von verknüpften Interop-Typen zurückgeben, auf dynamic und nicht auf object abgebildet, sodass kein Casting mehr erforderlich ist.

Was ist neu in C# 3.0

Die in C# 3.0 hinzugefügten Funktionen konzentrierten sich hauptsächlich auf die sprachintegrierte Abfrage (LINQ). LINQ ermöglicht es, Abfragen direkt in einem C#-Programm zu schreiben und statisch auf Korrektheit zu prüfen und sowohl lokale Sammlungen (wie Listen oder XML-Dokumente) als auch entfernte Datenquellen (wie eine Datenbank) abzufragen. Zu den in C# 3.0 hinzugefügten Funktionen zur Unterstützung von LINQ gehören implizit typisierte lokale Variablen, anonyme Typen, Objektinitialisierungen, Lambda-Ausdrücke, Erweiterungsmethoden, Abfrageausdrücke und Ausdrucksbäume.

Mit implizit typisierten lokalen Variablen (var Schlüsselwort, Kapitel 2) kannst du den Variablentyp in einer Deklarationsanweisung weglassen, damit der Compiler ihn ableiten kann. Das reduziert das Durcheinander und ermöglicht anonyme Typen(Kapitel 4), also einfache Klassen, die spontan erstellt werden und häufig in der Endausgabe von LINQ-Abfragen verwendet werden. Du kannst auch Arrays implizit typisieren(Kapitel 2).

Objektinitialisierer(Kapitel 3) vereinfachen die Objektkonstruktion, indem sie es dir ermöglichen, Eigenschaften inline nach dem Konstruktoraufruf zu setzen. Objektinitialisierer funktionieren sowohl mit benannten als auch mit anonymen Typen.

Lambda-Ausdrücke(Kapitel 4) sind Miniaturfunktionen, die vom Compiler im laufenden Betrieb erstellt werden; sie sind besonders nützlich bei "fließenden" LINQ-Abfragen(Kapitel 8).

Erweiterungsmethoden(Kapitel 4) erweitern einen bestehenden Typ um neue Methoden (ohne die Definition des Typs zu ändern), sodass sich statische Methoden wie Instanzmethoden anfühlen. Die Abfrageoperatoren von LINQ sind als Erweiterungsmethoden implementiert.

Abfrageausdrücke(Kapitel 8) bieten eine übergeordnete Syntax zum Schreiben von LINQ-Abfragen, die bei der Arbeit mit mehreren Sequenzen oder Bereichsvariablen wesentlich einfacher sein kann.

Expression Trees(Kapitel 8) sind Miniatur-Code Document Object Models (DOMs), die Lambda-Ausdrücke beschreiben, die dem speziellen Typ Expression<TDelegate> zugeordnet sind. Ausdrucksbäume ermöglichen die Ausführung von LINQ-Abfragen aus der Ferne (z. B. auf einem Datenbankserver), da sie zur Laufzeit introspektiert und übersetzt werden können (z. B. in eine SQL-Anweisung).

C# 3.0 fügte auch automatische Eigenschaften und partielle Methoden hinzu.

Automatische Eigenschaften(Kapitel 3) ersparen die Arbeit beim Schreiben von Eigenschaften, die einfach get/set ein privates Hintergrundfeld enthalten, indem sie den Compiler diese Arbeit automatisch erledigen lassen. Partielle Methoden(Kapitel 3) ermöglichen es, dass eine automatisch erzeugte partielle Klasse anpassbare Haken für die manuelle Erstellung bietet, die bei Nichtbenutzung "verschwinden".

Was ist neu in C# 2.0

Die großen Neuerungen in C# 2 waren Generics(Kapitel 3), nullable value types(Kapitel 4), Iteratoren(Kapitel 4) und anonyme Methoden (der Vorgänger der Lambda-Ausdrücke). Diese Funktionen ebneten den Weg für die Einführung von LINQ in C# 3.

C# 2 unterstützte außerdem partielle Klassen, statische Klassen und eine Reihe kleinerer und verschiedener Funktionen wie den Namespace Alias Qualifier, Friend Assemblies und Puffer mit fester Größe.

Die Einführung von Generika erforderte eine neue CLR (CLR 2.0), da Generika die volle Typentreue zur Laufzeit erhalten.

Get C# 12 in einer Kurzfassung now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.