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 umfassende Implementierung 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, dieselbe 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, lehnt sie sich auch an das funktionale Programmierparadigma an, genauer gesagt:

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 auch 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# erlaubt 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 eine Funktion, die eine ganze Zahl akzeptiert, nicht 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 Desktop 7-10 (für Rich-Client-, Web-, Server- und Befehlszeilenanwendungen)

  • Linux und macOS (für Web- und Kommandozeilenanwendungen)

  • Android und iOS (für mobile Anwendungen)

  • Windows 10 Geräte (Xbox, Surface Hub und HoloLens)

Es gibt auch eine Technologie namens Blazor, die C# zu Web-Assembly kompiliert und in einem Browser ausgeführt werden kann.

CLRs, BCLs und Laufzeiten

Die Laufzeitunterstützung für C#-Programme besteht aus einer Common Language Runtime und einer Base Class Library. Eine Laufzeitumgebung 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 JetBrains kannst du noch weiter gehen und die IL in C# dekompilieren. Da IL höherwertiger ist als nativer Maschinencode, 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 lässt dich explizit auf Funktionen der CLR zugreifen, 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 davon abhängig ist. 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 5 Web Windows, Linux, macOS
Windows-Desktop .NET 5 Windows Windows 7-10
Xamarin.iOS Mono 6 Mobil iOS
Xamarin.Android Mono 6 Mobil Android
UWP .NET Core 2.2 Win10 + Win10 Geräte Windows 10 Desktop & Geräte
(Veraltetes) .NET Framework .NET Framework Web, Windows Windows 7-10
Hinweis

Microsoft plant, dass der Nachfolger von .NET 5 - .NET 6 - die CLR/BCL für alle Laufzeiten außer .NET Framework wird. Das wird die Landschaft vereinfachen und das Schreiben von plattformübergreifenden Bibliotheken erleichtern. In der Zwischenzeit kannst du plattformübergreifende Bibliotheken schreiben, indem du den .NET Standard 2.0 verwendest (siehe ".NET Standard 2.0").

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 5

.NET 5 ist das Flaggschiff der Open-Source-Laufzeitumgebung von Microsoft. Du kannst Web- und Konsolenanwendungen schreiben, die unter Windows, Linux und macOS laufen - und Rich-Client-Anwendungen, die unter Windows 7 bis 10 laufen. Dieses Buch konzentriert sich auf die .NET 5 CLR und BCL.

Anders als .NET Framework ist .NET 5 auf Windows-Rechnern nicht vorinstalliert. Wenn du versuchst, eine .NET 5-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

.NET 5 ist eine neuere Version von .NET Core 3 (Microsoft hat "Core" aus dem Namen entfernt und Version 4 übersprungen). Der Grund für das Überspringen einer Version ist, dass eine Verwechslung mit .NET Framework 4.x vermieden werden soll.

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

.NET 5 ist .NET Core 3 sehr ähnlich, wobei sich die Unterschiede hauptsächlich auf die Leistung und die Bereitstellung konzentrieren.

Xamarin

Xamarin läuft auf iOS und Android und basiert auf einer Version der Open Source CLR/BCL von Mono. Microsoft plant für die nächste Version die Unterstützung von Desktop-Betriebssystemen, einschließlich macOS, und eine CLR/BCL, die vollständig mit .NET 6 kompatibel ist.

UWP

DieUniversal Windows Platform (UWP) wurde für das Schreiben von immersiven Touch-First-Anwendungen entwickelt, die auf dem Windows 10-Desktop und auf Geräten (Xbox, Surface Hub und HoloLens) laufen. UWP-Apps sind sandboxed und werden über den Windows Store ausgeliefert. UWP ist bei Windows 10 vorinstalliert.

Im Moment ist UWP noch auf die .NET Core 2.2 CLR/BCL beschränkt. Ihr Nachfolger, WinUI 3, wird auf .NET 6 aufbauen und sich besser in die .NET-Desktop-APIs integrieren. Mehr dazu in "UWP und WinUI 3" in Kapitel 5.

.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. In .NET Framework geschriebene Anwendungen können unter .NET 5 (oder .NET Core 3) neu kompiliert werden, obwohl sie in der Regel einige Änderungen erfordern. Einige Funktionen von .NET Framework sind in .NET 5 nicht vorhanden (und umgekehrt).

Das .NET Framework ist bei Windows vorinstalliert und wird automatisch über Windows Update gepatcht. Wenn du auf .NET Framework 4.8 abzielst, kannst du die Funktionen von C# 7.3 und früher nutzen.

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 oder .NET 5+ bezeichnen. 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" statt " Framework".

Legacy- und Nischen-Laufzeiten

Die folgenden Legacy-Laufzeiten sind noch verfügbar:

  • .NET Core 3.0 und 3.1 (abgelöst durch .NET 5)

  • .NET Core 1.x und 2.x (nur für Web- und Befehlszeilenanwendungen)

  • Windows Runtime für Windows 8/8.1 (jetzt UWP)

  • Microsoft XNA für die Spieleentwicklung (jetzt UWP)

Außerdem gibt es die folgenden Nischenlaufzeiten:

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

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

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# 9.0

C# 9.0 ist im Lieferumfang von Visual Studio 2019 enthalten und wird verwendet, wenn du auf .NET 5 abzielst.

Top-Level-Anweisungen

Mit Top-Level-Anweisungen (siehe "Top-Level-Anweisungen (C# 9)" in Kapitel 2) 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 (C# 9)" in Kapitel 3) 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 anstelle eines Konstruktors gefüllt werden können, und hilft, das Anti-Muster 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 (C# 9)" in Kapitel 4) ist eine besondere Art von Klasse, die für unveränderliche Daten ausgelegt 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 (int X, int 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 relationalen Muster (siehe "Muster" in Kapitel 4) 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 "Zielgerichtete neue Ausdrücke (C# 9)" in Kapitel 2.

Interop-Verbesserungen

C# 9 führt Funktionszeiger ein (siehe "Funktionszeiger (C# 9)" in Kapitel 4 und "Rückrufe mit Funktionszeigern (C# 9)" in Kapitel 24). Ihr Hauptzweck ist es, nicht verwalteten Code zu ermöglichen, statische Methoden in C# ohne den Overhead einer Delegateninstanz aufzurufen und dabei die P/Invoke-Schicht zu umgehen, 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 (C# 9)" in Kapitel 2), die zur Laufzeit auf System.IntPtr und System.UIntPtr abbilden. 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 (C# 9)" in Kapitel 3)

  • Attribute auf lokale Funktionen anwenden (siehe "Attribute" in Kapitel 4)

  • 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 (C# 9)" in Kapitel 4)

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

  • Definieren Sie eine Modulinitialisierungsmethode, die einmal beim ersten Laden einer Assembly ausgeführt wird, indem Sie das Attribut [ModuleInitializer] auf eine (statische, parameterlose) Methode anwenden

  • Verwirf" (Unterstrich) als Argument für einen Lambda-Ausdruck verwenden

  • Schreibe erweiterte partielle Methoden, die zwingend erforderlich sind, um Szenarien wie die neuen Quellgeneratoren von Roslyn zu implementieren (siehe "Erweiterte partielle Methoden (C# 9)" in Kapitel 3)

  • ein Attribut auf Methoden, Typen oder Module anwenden, um zu verhindern, dass lokale Variablen von der Laufzeit initialisiert werden (siehe "[SkipLocalsInit] (C# 9)" in Kapitel 4)

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

Indizesund 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 Spannen" in Kapitel 2.

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 verhindern).

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" in Kapitel 3.

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 "Switch-Ausdrücke" in Kapitel 2.

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" in Kapitel 4). MitTupel-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 Wertetypen Wertetypen nullbar machen, bewirken nullbare Referenztypen das Gegenteil und machen Referenztypen (bis zu einem gewissen Grad) nicht-nullbar, um NullReferenceExceptionzu verhindern. 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 "Nullbare Referenztypen" in Kapitel 4.

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, die Verbesserung der 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. die Möglichkeit, ref locals neu zuzuweisen, keine Notwendigkeit, beim Indizieren von fixed Feldern zu pinnen, und die 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 verwenden 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" und "Ref Returns" in Kapitel 2 und "Ref Structs" in Kapitel 3.

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

In 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" in Kapitel 2):

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" in Kapitel 3):

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" in Kapitel 2). 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 lokaleMethode ist eine Methode, die innerhalb einer anderen Funktion deklariert wird (siehe "Lokale Methoden" in Kapitel 3):

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" in Kapitel 3). Während ein Konstruktor normalerweise eine Reihe von Werten (als Parameter) annimmt 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 im vorangegangenen Beispiel wie folgt schreiben (Ausnahmebehandlung ausgenommen):

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" in Kapitel 4). 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 http://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" in Kapitel 2) 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

Funktionenin Form von Ausdrücken (siehe "Methoden" in Kapitel 3) 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" in Kapitel 2) bietet eine prägnante Alternative zu string.Format:

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

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

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

Mit der Direktive using static (siehe "Namensräume" in Kapitel 2) 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. So wird verhindert, dass der Code zerbricht, 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 Probleme bei der Versionierung und Bereitstellung ein Ende haben. 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 und statische Klassen sowie 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# 9.0 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.