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

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 Frameworks.

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, 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 noch 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, nimmt sie auch Anleihen beim funktionalen Programmierparadigma, 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. Mit C# ist es auch einfach, schreibgeschützte Felder und Eigenschaften zu definieren, um unveränderliche (schreibgeschützte) Typen zu schreiben.

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 feststellen, 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

In der Vergangenheit wurde C# fast ausschließlich zum Schreiben von Code verwendet, der auf Windows-Plattformen läuft. Inzwischen haben Microsoft und andere Unternehmen aber auch in andere Plattformen investiert:

  • Das .NET Core Framework ermöglicht die Entwicklung von Webanwendungen unter Linux und macOS (und auch unter Windows).

  • Xamarin ermöglicht die Entwicklung mobiler Apps für iOS und Android.

  • Blazor kompiliert C# zu einer Web-Assembly, die in einem Browser ausgeführt werden kann.

Und zwar auf der Windows-Plattform:

  • .NET Core 3 ermöglicht die Entwicklung von Rich-Client- und Web-Anwendungen unter Windows 7 bis 10.

  • DieUniversal Windows Platform (UWP) unterstützt Windows 10 Desktop und Geräte wie Xbox, Surface Hub und Hololens.

C# und die Common Language Runtime

C# hängt von einer Common Language Runtime (CLR) ab, die wichtige Laufzeitdienste wie die automatische Speicherverwaltung und die Ausnahmebehandlung bereitstellt. (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 konvertiert den IL-Code in den nativen Code der Maschine, z. B. X86 oder X64, 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 verbessern (und um die Regeln des iOS App Stores bei der Entwicklung mit Xamarin 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 JetBrains dotPeek 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 AWL zur Laufzeit erzeugen(Reflection.Emit).

Frameworks und Basisklassenbibliotheken

Eine CLR wird nicht alleine ausgeliefert, sondern als Teil eines Frameworks, das eine Reihe von Standard-Assemblies enthält. Wenn du eine Anwendung schreibst, wählst du ein bestimmtes Framework aus. Das bedeutet, dass deine Anwendung die Funktionen des Frameworks nutzt und davon abhängt. Die Wahl des Frameworks bestimmt auch, welche Plattformen deine Anwendung unterstützen wird.

Ein Framework besteht aus drei Schichten, wie in Abbildung 1-1 dargestellt. Die Basisklassenbibliotheken (Base Class Libraries, BCL) liegen über der CLR und bieten Funktionen, die für jede Art von Anwendung nützlich sind (z. B. Sammlungen, XML/JSON, Ein-/Ausgabe [I/O], Vernetzung, Serialisierung und parallele Programmierung). Über der BCL liegen Anwendungsframeworks, die die APIs für ein Benutzeroberflächenparadigma bereitstellen (wie ASP.NET Core für eine Webanwendung oder Windows Presentation Foundation [WPF] für eine Rich-Client-Anwendung). Für ein Befehlszeilenprogramm ist keine Anwendungsschicht erforderlich.

Framework architecture
Abbildung 1-1. Rahmenarchitektur

Als C# im Jahr 2000 auf den Markt kam, gab es nur das Microsoft .NET Framework. Heute gibt es vier große Frameworks:

.NET Core
Modernes Open-Source-Framework zum Schreiben von Web- und Konsolenanwendungen, die unter Windows, Linux und macOS laufen, sowie Rich-Client-Anwendungen, die unter Windows 7 bis 10 (mit .NET Core 3+) laufen. Du kannst mehrere Versionen von .NET Core nebeneinander installieren, und Anwendungen können in sich geschlossen sein, sodass keine .NET Core-Installation erforderlich ist.
UWP
Für das Schreiben von immersiven Touch-First-Anwendungen, 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.
Mono + Xamarin
Open-Source-Framework zum Schreiben von mobilen Apps, die auf iOS und Android laufen.
.NET Framework (abgelöst durch .NET Core 3)
Für das Schreiben von Web- und Rich-Client-Anwendungen, die auf Windows-Desktop/Server ausgerichtet sind. Es sind keine größeren neuen Versionen geplant, obwohl Microsoft die aktuelle Version 4.8 aufgrund der Fülle an bestehenden Anwendungen weiterhin unterstützen und pflegen wird. Das .NET Framework ist in Windows vorinstalliert und unterstützt C# 7.3 und früher.

Obwohl sich diese Frameworks in ihrer Plattformunterstützung und ihrem Verwendungszweck unterscheiden, stellen sie alle eine ähnliche CLR und BCL zur Verfügung.

Hinweis

Du kannst dir diese Gemeinsamkeiten zunutze machen und Klassenbibliotheken schreiben, die in mehreren Frameworks funktionieren - siehe ".NET Standard" in Kapitel 5.

Dieses Buch konzentriert sich auf C# und die Kernfunktionen der CLR und BCL, wie in Abbildung 1-2 dargestellt. Auch wenn der Schwerpunkt auf .NET Core 3 liegt, behandeln wir auch einige der Windows Runtime-Typen für UWP-Anwendungen, die parallel zur BCL Funktionen bieten.

Topics covered in this book—the applications frameworks (shown in gray) are not covered
Abbildung 1-2. In diesem Buch behandelte Themen - die Anwendungsframeworks (grau dargestellt) werden nicht behandelt

Legacy- und Nischen-Frameworks

Die folgenden Frameworks sind noch verfügbar, um ältere Plattformen zu unterstützen:

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

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

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

Außerdem gibt es die folgenden Nischenrahmen:

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

  • Mono (auf dem Xamarin aufbaut) verfügt auch über eine Anwendungsschicht, um plattformübergreifende "Windows Forms"-Anwendungen für Linux, macOS und Windows zu entwickeln. Nicht alle Funktionen werden unterstützt oder funktionieren vollständig. (Eine weitere Option für die plattformübergreifende Entwicklung von Benutzeroberflächen ist Avalonia, eine WPF-inspirierte Bibliothek, die auf .NET Core und .NET Framework läuft).

  • 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.

Windows Laufzeit

C# arbeitet auch mit der Windows Runtime (WinRT) Technologie zusammen. WinRT bedeutet zwei Dinge:

  • Eine sprachneutrale objektorientierte Ausführungsschnittstelle, die von Windows 8 und höher unterstützt wird

  • Eine Reihe von Bibliotheken, die in Windows 8 und höher integriert sind und diese Ausführungsschnittstelle unterstützen

Hinweis

Etwas verwirrend ist, dass der Begriff WinRT in der Vergangenheit für zwei weitere Dinge verwendet wurde:

  • Der Vorgänger von UWP, d.h. die Entwicklungsplattform zum Schreiben von Store-Apps für Windows 8/8.1, manchmal auch "Metro" oder "Modern" genannt

  • Das nicht mehr existierende mobile Betriebssystem für RISC-basierte Tablets ("Windows RT"), das Microsoft 2011 veröffentlichte

Unter einer Ausführungsschnittstelle verstehen wir ein Protokoll für den Aufruf von Code, der (möglicherweise) in einer anderen Sprache geschrieben wurde. Microsoft Windows hat in der Vergangenheit eine primitive Ausführungsschnittstelle in Form von Low-Level-Funktionsaufrufen im C-Stil bereitgestellt, die die Win32-API umfasst.

WinRT ist viel umfangreicher. Zum Teil ist es eine erweiterte Version des Component Object Model (COM), die .NET, C++ und JavaScript unterstützt. Anders als Win32 ist es objektorientiert und hat ein relativ umfangreiches Typensystem. Das bedeutet, dass sich der Verweis auf eine WinRT-Bibliothek von C# aus ähnlich anfühlt wie der Verweis auf eine .NET-Bibliothek - du bist dir vielleicht nicht einmal bewusst, dass du WinRT verwendest.

Die WinRT-Bibliotheken in Windows 10 sind ein wesentlicher Bestandteil der UWP-Plattform (UWP stützt sich sowohl auf WinRT- als auch auf .NET Core-Bibliotheken). Wenn du auf die Standard-.NET Core-Plattform abzielst, ist der Verweis auf die Windows 10 WinRT-Bibliotheken optional und kann nützlich sein, wenn du auf Windows 10-spezifische Funktionen zugreifen musst, die sonst nicht in .NET Core enthalten sind.

Die WinRT-Bibliotheken in Windows 10 unterstützen die UWP-UI für das Schreiben von immersiven Touch-First-Anwendungen. Sie unterstützen auch mobile gerätespezifische Funktionen wie Sensoren, Textnachrichten usw. (die neuen Funktionen von Windows 8, 8.1 und 10 wurden über WinRT und nicht über Win32 bereitgestellt). WinRT-Bibliotheken bieten auch Dateieingabe und -ausgabe, die für die UWP-Sandbox geeignet sind.

Was WinRT von gewöhnlichem COM unterscheidet, ist, dass WinRT seine Bibliotheken in eine Vielzahl von Sprachen projiziert, nämlich C#, Visual Basic, C++ und JavaScript, sodass jede Sprache WinRT-Typen (fast) so sieht, als wären sie speziell für sie geschrieben worden. WinRT passt zum Beispiel die Regeln für die Großschreibung an die Standards der Zielsprache an und passt sogar einige Funktionen und Schnittstellen neu an. WinRT-Assemblies werden außerdem mit umfangreichen Metadaten in .winmd-Dateien ausgeliefert, die das gleiche Format wie .NET-Assembly-Dateien haben, so dass sie ohne besondere Rituale verwendet werden können; deshalb ist dir vielleicht gar nicht bewusst, dass du WinRT- und nicht .NET-Typen verwendest, abgesehen von den Unterschieden bei den Namensräumen. Ein weiterer Hinweis ist, dass WinRT-Typen COM-ähnlichen Beschränkungen unterliegen, z. B. bieten sie nur begrenzte Unterstützung für Vererbung und Generika.

In C# kannst du nicht nur WinRT-Bibliotheken verwenden, sondern auch deine eigenen schreiben (und sie aus einer JavaScript-Anwendung heraus aufrufen).

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

C# 8.0 wird mit Visual Studio 2019 ausgeliefert.

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 Ranges (C# 8)" in Kapitel 2.

Null-Koaleszenz-Zuweisung

Der ??= Operator weist eine Variable nur zu, wenn sie null ist. Stattdessen:

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

kannst du das jetzt schreiben:

s ??= "Hello, world";

Erklärungen 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 einschließ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 mit denen 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:

ILogger.Prefix = "File log: ";

es sei denn, die Erreichbarkeit wird durch einen Modifikator für das statische Schnittstellenmitglied eingeschränkt (z. B. private, protected oder internal). Instanzfelder sind verboten.

Weitere Informationen findest du unter "Standardschnittstellenmitglieder (C# 8)" in Kapitel 3.

Schalterausdrücke

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 (C# 8)" 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). Mit Tupel-Mustern kannst du mehrere Werte einschalten:

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 NullReferenceExceptions zu vermeiden. Nullbare Referenztypen führen eine Sicherheitsebene ein, die ausschließlich vom Compiler in Form von Warnungen oder Fehlern durchgesetzt wird, wenn er Code erkennt, der Gefahr läuft, eine Null​ReferenceException.

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 glaubt, dass ein Null​ReferenceException 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 (C# 8)" 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 (C# 8)" in Kapitel 14.

Was ist neu in C# 7.x

C# 7 wird mit Visual Studio 2017 ausgeliefert.

C# 7.3

Mit C# 7.3 wurden kleinere Verbesserungen an bestehenden Funktionen vorgenommen, z. B. die Verwendung von Gleichheitsoperatoren mit Tupeln, die 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 Wegfall der Pin-Pflicht bei der Indizierung 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 verwenden solltest - in Kapitel 24.

C# 7.2

C# 7.2 fügte einen neuen private protected Modifikator (die Schnittmenge von internal und protected), die Möglichkeit, beim Methodenaufruf benannte Argumente 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 Typ-Parameter anwenden kannst), die Main Methode 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 lokale Methode 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 ausdrucksbasierte Fat-Arrow-Syntax für Methoden, schreibgeschützte Eigenschaften, Operatoren und Indexer eingeführt. C# 7 erweitert diese Syntax auf Konstruktoren, Lese-/Schreibeigenschaften und Finalisierer:

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 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" 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 in Form von Bibliotheken zur Verfügung und ermöglicht es dir, Codeanalysen an beliebigem Quellcode durchzuführen (siehe Kapitel 27). Der Compiler selbst ist Open Source, und der Quellcode ist auf GitHub 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

Funktionen in 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. 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 im Detail 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 20) 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 25) 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 Äquivalenz der Typen, 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 nach dem Konstruktoraufruf inline 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.

Ausdrucksbäume(Kapitel 8) sind Miniatur-Code Document Object Models (DOMs), die Lambda-Ausdrücke beschreiben, die dem speziellen Typ Expression​<TDelegate>. 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# 8.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.