Kapitel 1. Einführung in C#

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

Die Programmiersprache C# (ausgesprochen "see sharp") wird für viele Arten von Anwendungen verwendet, darunter Websites, Cloud-basierte Systeme, IoT-Geräte, maschinelles Lernen, Desktop-Anwendungen, eingebettete Steuerungen, mobile Apps, Spiele und Kommandozeilenprogramme. C# und die dazugehörige Laufzeitumgebung, Bibliotheken und Tools, die unter dem Namen .NET bekannt sind, stehen seit über 20 Jahren im Mittelpunkt der Arbeit von Windows-Entwicklern. Heute ist .NET plattformübergreifend und quelloffen, so dass in C# geschriebene Anwendungen und Dienste nicht nur unter Windows, sondern auch unter Betriebssystemen wie Android, iOS, macOS und Linux laufen.

Die Veröffentlichung von C# 10.0 und der dazugehörigen Laufzeitumgebung .NET 6.0 markiert einen wichtigen Meilenstein: Die Entwicklung von C# zu einer vollständig plattformübergreifenden Open-Source-Sprache ist nun abgeschlossen. Obwohl es in der Geschichte von C# schon immer Open-Source-Implementierungen gab, begann 2016 ein Umbruch, als Microsoft .NET Core 1.0 veröffentlichte, die erste von Microsoft vollständig unterstützte Plattform, auf der C# nicht nur unter Windows, sondern auch unter Linux und macOS läuft. Die Unterstützung von Bibliotheken und Tools für .NET Core war anfangs lückenhaft, so dass Microsoft weiterhin neue Versionen seiner älteren Laufzeitumgebung, des Closed-Source-Frameworks für Windows, auslieferte, aber sechs Jahre später wird diese alte Laufzeitumgebung praktisch in Rente geschickt,1 Mit .NET 5.0 wurde das "Core" aus dem Namen gestrichen, um zu verdeutlichen, dass es jetzt das Hauptereignis ist, aber erst mit .NET 6.0 ist die plattformübergreifende Version wirklich angekommen, denn diese Version genießt den vollen Long Term Support (LTS) Status. Zum ersten Mal hat die plattformunabhängige Version von C# und .NET das alte .NET Framework abgelöst.

C# und .NET sind Open-Source-Projekte, auch wenn das nicht von Anfang an so war. In der Anfangszeit von C# hat Microsoft den gesamten Quellcode streng gehütet, aber 2014 wurde die .NET Foundation gegründet, um die Entwicklung von Open-Source-Projekten in der .NET-Welt zu fördern. Viele der wichtigsten C#- und .NET-Projekte von Microsoft werden nun von der Foundation verwaltet (zusätzlich zu vielen Nicht-Microsoft-Projekten). Dazu gehören der C#-Compiler von Microsoft und auch die .NET-Laufzeitumgebung und -Bibliotheken. Heute wird so ziemlich alles, was mit C# zu tun hat, offen entwickelt, wobei Codebeiträge von außerhalb Microsofts willkommen sind. Vorschläge für neue Sprachfunktionen werden auf GitHub verwaltet, so dass die Gemeinschaft schon in der Anfangsphase einbezogen werden kann.

Warum C#?

Obwohl es viele Möglichkeiten gibt, C# zu verwenden, sind andere Sprachen immer eine Option. Warum solltest du C# gegenüber anderen Sprachen bevorzugen? Das hängt davon ab, was du tun musst und was du an einer Programmiersprache magst oder nicht magst. Ich finde, dass C# sehr mächtig, flexibel und leistungsfähig ist und auf einer so hohen Abstraktionsebene arbeitet, dass ich nicht viel Aufwand für kleine Details betreiben muss, die nicht direkt mit den Problemen zu tun haben, die meine Programme lösen sollen.

Ein großer Teil der Stärke von C# liegt in der Bandbreite der Programmiertechniken, die es unterstützt. Es bietet zum Beispiel objektorientierte Funktionen, Generics und funktionale Programmierung. Es unterstützt sowohl dynamische als auch statische Typisierung. Dank Language Integrated Query (LINQ) bietet es leistungsstarke listen- und mengenorientierte Funktionen. Asynchrone Programmierung wird von Haus aus unterstützt. Außerdem bieten die verschiedenen Entwicklungsumgebungen, die C# unterstützen, eine breite Palette an produktivitätssteigernden Funktionen.

C# bietet Optionen, um einen Ausgleich zwischen einfacher Entwicklung und Leistung zu schaffen. Die Laufzeitumgebung bietet seit jeher einen Garbage Collector (GC), der den Entwicklern einen Großteil der Arbeit abnimmt, die mit der Wiederherstellung von Speicher verbunden ist, den das Programm nicht mehr benötigt. Ein GC ist eine gängige Funktion in modernen Programmiersprachen, und obwohl er für die meisten Programme ein Segen ist, gibt es einige spezielle Szenarien, in denen seine Auswirkungen auf die Leistung problematisch sind. Daher ermöglicht C# eine explizitere Speicherverwaltung, die dir die Möglichkeit gibt, die einfache Entwicklung gegen die Laufzeitleistung einzutauschen, ohne dabei die Typsicherheit zu verlieren. Dadurch eignet sich C# für bestimmte leistungskritische Anwendungen, die jahrelang weniger sicheren Sprachen wie C und C++ vorbehalten waren.

Sprachen existieren nicht im luftleeren Raum - hochwertige Bibliotheken mit einer breiten Palette von Funktionen sind unerlässlich. Einige elegante und akademisch schöne Sprachen sind glorreich, bis du etwas Prosaisches tun willst, wie z. B. mit einer Datenbank kommunizieren oder festlegen, wo die Benutzereinstellungen gespeichert werden sollen. Unabhängig davon, wie leistungsfähig eine Programmiersprache ist, muss sie auch einen umfassenden und bequemen Zugang zu den Diensten der zugrunde liegenden Plattform bieten. C# ist hier dank seiner Laufzeitumgebung, der eingebauten Klassenbibliotheken und der umfangreichen Unterstützung von Drittanbieterbibliotheken sehr gut aufgestellt.

.NET umfasst sowohl die Laufzeit- als auch die Hauptklassenbibliotheken, die C#-Programme verwenden. Der Runtime-Teil wird Common Language Runtime (abgekürzt CLR) genannt, weil er nicht nur C#, sondern jede .NET-Sprache unterstützt. Microsoft bietet z.B. auch Visual Basic, F# und .NET-Erweiterungen für C++ an. Die CLR verfügt über ein Common Type System (CTS), das es ermöglicht, dass Code aus verschiedenen Sprachen frei miteinander interagieren kann. Das bedeutet, dass .NET-Bibliotheken normalerweise von jeder .NET-Sprache verwendet werden können - F# kann in C# geschriebene Bibliotheken nutzen, C# kann Visual Basic-Bibliotheken verwenden und so weiter.

In .NET ist ein umfangreicher Satz von Klassenbibliotheken integriert. Diese haben im Laufe der Jahre verschiedene Namen erhalten, darunter Base Class Library (BCL), Framework Class Library und Framework-Bibliotheken, aber Microsoft scheint sich jetzt auf Laufzeitbibliotheken als Bezeichnung für diesen Teil von .NET geeinigt zu haben. Diese Bibliotheken bieten Wrapper für viele Funktionen des zugrundeliegenden Betriebssystems (OS), aber sie bieten auch eine beträchtliche Menge an eigenen Funktionen, wie z. B. Sammelklassen und JSON-Verarbeitung.

Die .NET-Laufzeitklassenbibliotheken sind nicht alles - viele andere Systeme bieten ihre eigenen .NET-Bibliotheken an. Zum Beispiel gibt es Bibliotheken, mit denen C#-Programme beliebte Cloud-Dienste nutzen können. Wie zu erwarten, bietet Microsoft umfassende .NET-Bibliotheken für die Arbeit mit den Diensten seiner Azure-Cloud-Plattform an. Auch Amazon bietet ein umfassendes Entwicklungskit für die Nutzung der Amazon Web Services (AWS) in C# und anderen .NET-Sprachen an. Und die Bibliotheken müssen nicht mit bestimmten Diensten verbunden sein. Es gibt ein großes Ökosystem von .NET-Bibliotheken, einige kommerziell, andere kostenlos, darunter mathematische Hilfsprogramme, Parsing-Bibliotheken und Komponenten für die Benutzeroberfläche (UI), um nur einige zu nennen. Selbst wenn du das Pech hast, eine Funktion des Betriebssystems nutzen zu müssen, für die es keine .NET-Bibliotheks-Wrapper gibt, bietet C# verschiedene Mechanismen, um mit anderen Arten von APIs zu arbeiten, z. B. mit den C-ähnlichen APIs von Win32, macOS und Linux oder mit APIs, die auf dem Component Object Model (COM) von Windows basieren.

Neben den Bibliotheken gibt es auch zahlreiche Anwendungs-Frameworks. .NET verfügt über integrierte Frameworks für die Erstellung von Webanwendungen und Web-APIs, Desktop-Anwendungen und mobilen Anwendungen. Außerdem gibt es Open-Source-Frameworks für verschiedene Arten der Entwicklung verteilter Systeme, z. B. für die hochvolumige Ereignisverarbeitung mit Reaqtor oder hochverfügbare, global verteilte Systeme mit Orleans.

Da es .NET seit mehr als zwei Jahrzehnten gibt, haben viele Unternehmen umfangreich in Technologien investiert, die auf dieser Plattform aufbauen. Daher ist C# oft die erste Wahl, um die Früchte dieser Investitionen zu ernten.

Zusammenfassend lässt sich sagen, dass wir mit C# einen starken Satz von Abstraktionen in die Sprache integriert haben, eine leistungsstarke Laufzeitumgebung und einen einfachen Zugang zu einer enormen Menge an Bibliotheks- undPlattformfunktionen.

Verwalteter Code und die CLR

C# war die erste Sprache, die als native Sprache in der Welt der CLR entwickelt wurde. Das gibt C# ein unverwechselbares Gefühl. Das bedeutet auch, dass du die CLR und die Art und Weise, wie sie Code ausführt, verstehen musst, wenn du C# verstehen willst.

Jahrelang bestand die übliche Arbeitsweise eines Compilers darin, den Quellcode zu verarbeiten und eine Ausgabe zu erzeugen, die direkt von der CPU des Computers ausgeführt werden konnte. Compiler erzeugen Maschinencode - eineReihe von Anweisungen in dem Binärformat, das für die CPU des Computers erforderlich ist. Viele Compiler arbeiten immer noch auf diese Weise, aber der C#-Compiler tut das nicht. Stattdessen verwendet er ein Modell namens Managed Code.

Bei verwaltetem Code erzeugt der Compiler nicht den Maschinencode, der von der CPU ausgeführt wird. Stattdessen erzeugt der Compiler eine Form von Binärcode, die Zwischensprache (IL). Der ausführbare Binärcode wird später erzeugt, normalerweise, aber nicht immer, zur Laufzeit. Die Verwendung von IL ermöglicht Funktionen, die mit dem traditionellen Modell nur schwer oder gar nicht möglich sind.

Der vielleicht sichtbarste Vorteil des verwalteten Modells ist, dass die Ausgabe des Compilers nicht an eine einzige CPU-Architektur gebunden ist. Die in den meisten modernen Computern verwendeten CPUs unterstützen beispielsweise sowohl 32-Bit- als auch 64-Bit-Befehlssätze (aus historischen Gründen als x86 bzw. x64 bekannt). Mit dem alten Modell der Kompilierung von Quellcode in Maschinensprache musstest du dich entscheiden, welchen dieser beiden Sätze du unterstützen willst, und mehrere Versionen deiner Komponente erstellen, wenn du mehr als eine ansprechen willst. Mit .NET kannst du jedoch eine einzige Komponente erstellen, die ohne Änderungen sowohl in 32-Bit- als auch in 64-Bit-Prozessen ausgeführt werden kann. Dieselbe Komponente kann sogar auf völlig unterschiedlichen Architekturen laufen, wie z. B. ARM (eine Prozessorarchitektur, die in Mobiltelefonen, neueren Macs und auch in kleinen Geräten wie dem Raspberry Pi weit verbreitet ist). Mit einer Sprache, die sich direkt in Maschinencode kompilieren lässt, müsstest du für jede dieser Architekturen unterschiedliche Binärdateien erstellen oder in manchen Fällen eine einzige Datei, die mehrere Kopien des Codes enthält, eine für jede unterstützte Architektur. Mit .NET kannst du eine einzige Komponente kompilieren, die nur eine Version des Codes enthält und auf jeder von ihnen läuft. Sie kann sogar auf Plattformen laufen, die zum Zeitpunkt der Kompilierung nicht unterstützt wurden, wenn in der Zukunft eine geeignete Laufzeitumgebung verfügbar wird. (Zum Beispiel können .NET-Komponenten, die Jahre vor der Veröffentlichung der ersten ARM-basierten Macs von Apple geschrieben wurden, nativ ausgeführt werden, ohne auf die Rosetta-Übersetzungstechnologie angewiesen zu sein, die es normalerweise ermöglicht, dass älterer Code auf den neueren Prozessoren funktioniert.) Generell gilt, dass jede Verbesserung der Codegenerierung der CLR - sei es die Unterstützung neuer CPU-Architekturen oder die Leistungsverbesserung bestehender Architekturen - sofort allen .NET-Sprachen zugute kommt. Ältere Versionen der CLR nutzten zum Beispiel nicht die Vorteile der Vektorverarbeitungserweiterungen, die auf modernen x86- und x64-Prozessoren verfügbar sind. Der gesamte Code, der auf aktuellen Versionen von .NET läuft, profitiert davon, auch Komponenten, die Jahre vor der Einführung dieser Verbesserung erstellt wurden.

Der genaue Zeitpunkt, zu dem die CLR ausführbaren Maschinencode erzeugt, kann variieren. In der Regel wird ein Ansatz verwendet, der als Just-in-Time-Kompilierung (JIT) bezeichnet wird und bei dem der Maschinencode jeder einzelnen Funktion bei der ersten Ausführung erzeugt wird. Das muss aber nicht zwangsläufig so funktionieren. Eine der Laufzeitimplementierungen, Mono, kann IL direkt interpretieren, ohne sie in lauffähige Maschinensprache umzuwandeln. Das ist auf Plattformen wie iOS nützlich, wo rechtliche Einschränkungen eine JIT-Kompilierung verhindern können. Das .NET Software Development Kit (SDK) bietet auch ein Tool namens crossgen, mit dem du vorkompilierten Code neben der IL erstellen kannst. DieseA oT-Kompilierung (AoT =ahead-of-time ) kann die Startzeit einer Anwendung verbessern. Es gibt auch eine separate Laufzeitumgebung namens .NET Native, die nur die Vorkompilierung unterstützt und von Windows Store Apps verwendet wird, die für die Universal Windows Platform (UWP) entwickelt wurden. (Microsoft hat angekündigt, dass die reine Windows .NET Native Laufzeitumgebung wahrscheinlich auslaufen und durch NativeAOT, ihren plattformübergreifenden Nachfolger, ersetzt werden wird).

Hinweis

Auch wenn du den Code mit crossgen vorkompilierst, kann die Generierung von ausführbarem Code noch zur Laufzeit erfolgen. Die abgestufte Kompilierungsfunktion der CLR kann eine Methode dynamisch neu kompilieren, um sie besser für die Verwendung zur Laufzeit zu optimieren, egal ob du JIT oder AoT verwendest.2

Verwalteter Code hat allgegenwärtige Typinformationen. Die .NET-Laufzeitumgebung benötigt diese Informationen, weil sie bestimmte Laufzeitfunktionen ermöglichen. So bietet .NET zum Beispiel verschiedene automatische Serialisierungsdienste an, mit denen Objekte in binäre oder textuelle Darstellungen ihres Zustands umgewandelt werden können, die später wieder in Objekte umgewandelt werden können, vielleicht auf einem anderen Rechner. Diese Art von Diensten setzt eine vollständige und genaue Beschreibung der Struktur eines Objekts voraus, die im verwalteten Code garantiert vorhanden ist. Typinformationen können auch auf andere Weise genutzt werden. Unit-Test-Frameworks können sie zum Beispiel nutzen, um den Code in einem Testprojekt zu untersuchen und alle von dir geschriebenen Unit-Tests zu finden. Dies beruht auf den Reflection Services der CLR, die in Kapitel 13 behandelt werden.

Obwohl die enge Verbindung mit der Laufzeitumgebung eines der wichtigsten Merkmale von C# ist, ist es nicht das einzige. Dem Design von C# liegt eine bestimmte Philosophie zugrunde.

C# bevorzugt Allgemeinheit gegenüber Spezialisierung

C# bevorzugt allgemeine Sprachfunktionen gegenüber speziellen. C# befindet sich mittlerweile in der 10. Hauptversion, und bei jeder neuen Version hatten die Designer der Sprache bestimmte Szenarien im Kopf, als sie neue Funktionen entwarfen. Sie haben sich jedoch stets bemüht, sicherzustellen, dass jedes Element, das sie hinzufügen, auch über diese primären Szenarien hinaus nützlich ist.

Vor ein paar Jahren haben die Entwickler von C# zum Beispiel beschlossen, C# um Funktionen zu erweitern, die den Datenbankzugriff in die Sprache integrieren. Die daraus resultierende Technologie, Language Integrated Query (LINQ, beschrieben in Kapitel 10), unterstützt dieses Ziel, ohne jedoch den Datenzugriff direkt in die Sprache zu integrieren. Stattdessen hat das Entwicklungsteam eine Reihe von ganz unterschiedlich anmutenden Funktionen eingeführt. Dazu gehören eine bessere Unterstützung für funktionale Programmiersprachen, die Möglichkeit, neue Methoden zu bestehenden Typen hinzuzufügen, ohne auf Vererbung zurückgreifen zu müssen, die Unterstützung anonymer Typen, die Möglichkeit, ein Objektmodell zu erhalten, das die Struktur eines Ausdrucks darstellt, und die Einführung einer Abfragesyntax. Der letzte Punkt hat einen offensichtlichen Bezug zum Datenzugriff, aber die anderen sind schwieriger mit der vorliegenden Aufgabe zu verbinden. Nichtsdestotrotz können sie zusammen verwendet werden, um bestimmte Datenzugriffsaufgaben erheblich zu vereinfachen. Die Funktionen sind aber auch alle für sich genommen nützlich, sodass sie nicht nur den Datenzugriff unterstützen, sondern auch eine viel breitere Palette von Szenarien ermöglichen. Diese Ergänzungen machen es zum Beispiel viel einfacher, Listen, Mengen und andere Gruppen von Objekten zu verarbeiten, denn die neuen Funktionen funktionieren für Sammlungen von Dingen jeglicher Herkunft, nicht nur von Datenbanken.

Ein Beispiel für diese Philosophie der Allgemeingültigkeit ist ein Sprachfeature, das als Prototyp für C# entwickelt wurde, das die Entwickler aber letztendlich nicht weiterverfolgt haben. Die Funktion hätte es dir ermöglicht, XML direkt in deinem Quellcode zu schreiben und Ausdrücke einzubetten, um Werte für bestimmte Inhalte zur Laufzeit zu berechnen. Der Prototyp kompilierte dies in einen Code, der das fertige XML zur Laufzeit generierte. Microsoft Research hat dies öffentlich demonstriert, aber diese Funktion hat es letztendlich nicht in C# geschafft, obwohl sie später in eine andere .NET-Sprache, Visual Basic, integriert wurde, die auch einige spezielle Abfragefunktionen zum Extrahieren von Informationen aus XML-Dokumenten erhielt. Eingebettete XML-Ausdrücke sind eine relativ begrenzte Funktion, die nur beim Erstellen von XML-Dokumenten nützlich ist. Was die Abfrage von XML-Dokumenten angeht, so unterstützt C# diese Funktion durch seine allgemeinen LINQ-Funktionen, ohne dass dafür XML-spezifische Sprachfunktionen erforderlich sind. Der Stern von XML ist seit der Entwicklung dieses Sprachkonzepts gesunken, da es in vielen Fällen von JSON verdrängt wurde (das in den nächsten Jahren zweifellos von etwas anderem verdrängt werden wird). Hätte es eingebettetes XML in C# geschafft, würde es sich inzwischen wie eine leicht anachronistische Kuriosität anfühlen.

Die neuen Funktionen, die in den nachfolgenden Versionen von C# hinzugefügt wurden, gehen in dieselbe Richtung. Die Dekonstruktions- und Pattern-Matching-Funktionen, die in den letzten Versionen von C# hinzugefügt wurden, sollen das Leben auf subtile, aber nützliche Weise erleichtern und sind nicht auf einen bestimmten Anwendungsbereich beschränkt.

C# Standards und Implementierungen

Bevor wir mit dem eigentlichen Code loslegen können, müssen wir wissen, welche C#-Implementierung und welche Laufzeit wir anvisieren. Die Standardisierungsorganisation Ecma hat Spezifikationen verfasst, die das Sprach- und Laufzeitverhalten (ECMA-334 bzw. ECMA-335) für C#-Implementierungen festlegen. Dies hat es möglich gemacht, dass mehrere Implementierungen von C# und der Laufzeitumgebung entstanden sind. Zum Zeitpunkt der Erstellung dieses Artikels sind vier davon weit verbreitet: Mono, .NET Native, .NET (früher bekannt als .NET Core) und .NET Framework. Etwas verwirrend ist, dass Microsoft hinter all diesen Produkten steht, obwohl das ursprünglich nicht so war.

Viele .NETs

Das Mono-Projekt wurde 2001 ins Leben gerufen und stammt nicht von Microsoft (deshalb trägt es auch kein .NET in seinem Namen - es kann den Namen C# verwenden, weil die Standards die Sprache so nennen, aber in der Zeit vor der .NET Foundation wurde die Marke .NET ausschließlich von Microsoft verwendet). Mono begann mit dem Ziel, die Entwicklung von Linux-Desktop-Anwendungen in C# zu ermöglichen, aber später wurde die Unterstützung für iOS und Android hinzugefügt. Dieser entscheidende Schritt hat Mono geholfen, seine Nische zu finden, denn jetzt wird es hauptsächlich für die Entwicklung plattformübergreifender Anwendungen für mobile Geräte in C# verwendet. Mono unterstützt jetzt auch WebAssembly (auch bekannt als WASM) und enthält eine Implementierung der CLR, die in jedem standardkonformen Webbrowser ausgeführt werden kann, sodass C#-Code in Webanwendungen auf der Client-Seite ausgeführt werden kann. Es wird oft in Verbindung mit einem .NET-Anwendungsframework namens Blazor verwendet, das es dir ermöglicht, HTML-basierte Benutzeroberflächen zu erstellen und gleichzeitig C# zur Implementierung des Verhaltens zu verwenden. Die Kombination aus Blazor und WASM macht C# auch zu einer brauchbaren Sprache für die Arbeit mit Plattformen wie Electron, die Web-Client-Technologien nutzen, um plattformübergreifende Desktop-Anwendungen zu erstellen. (Blazor erfordert kein WASM - es kann auch mit normal kompiliertem C#-Code arbeiten, der auf der .NET-Laufzeitumgebung läuft; dies ist die Grundlage für die Multiplattform-App-UI (MAUI) von .NET, die es ermöglicht, eine einzige Anwendung zu schreiben, die auf Android, iOS, macOS und Windows laufen kann).

Mono war von Anfang an Open Source und wurde im Laufe seines Bestehens von einer Vielzahl von Unternehmen unterstützt. Im Jahr 2016 übernahm Microsoft das Unternehmen, das für Mono verantwortlich war: Xamarin. Bis auf Weiteres behält Microsoft Xamarin als eigenständige Marke bei und positioniert es als die Möglichkeit, plattformübergreifende C#-Anwendungen zu schreiben, die auf mobilen Geräten ausgeführt werden können. Die Kerntechnologie von Mono wurde in die .NET-Laufzeitcodebasis von Microsoft integriert. Dies war der Endpunkt einer mehrjährigen Konvergenzphase, in der Mono nach und nach immer mehr Gemeinsamkeiten mit .NET aufwies. Anfangs stellte Mono seine eigenen Implementierungen von allem bereit: C#-Compiler, Bibliotheken und die CLR. Aber als Microsoft eine Open-Source-Version seines eigenen Compilers veröffentlichte, wurden die Mono-Tools auf diesen umgestellt. Früher hatte Mono seine eigene vollständige Implementierung der .NET-Laufzeitbibliotheken, aber seit Microsoft die Open-Source-Version von .NET Core veröffentlicht hat, ist Mono zunehmend davon abhängig. Heute ist Mono eine von zwei CLR-Implementierungen im Haupt-Repository der .NET-Laufzeitbibliotheken, die Unterstützung für mobile und WebAssembly-Laufzeitumgebungen bieten.

Was ist mit den anderen drei Implementierungen, die alle als .NET bezeichnet werden? Es gibt .NET Native, das in UWP-Apps verwendet wird. Wie im vorangegangenen Abschnitt beschrieben, ist dies eine spezielle Version von .NET, die nur die AoT-Kompilierung unterstützt. Allerdings soll .NET Native durch NativeAOT ersetzt werden, das eine Funktion von .NET sein wird und keine völlig separate Implementierung. In der Praxis gibt es also nur zwei aktuelle, nicht verdammte Versionen: .NET Framework (nur Windows, Closed-Source) und .NET (plattformübergreifend, Open Source; früher .NET Core genannt). Wie bereits erwähnt, plant Microsoft jedoch nicht, das reine Windows-Framework um neue Funktionen zu erweitern, so dass .NET 6.0 die einzige aktuelle Version ist.

Diese Konvergenz zu einer aktuellen Hauptversion war eines der Hauptziele von .NET 6 und macht es zu einer besonders wichtigen Version. Dennoch ist es nützlich, über die anderen Versionen Bescheid zu wissen, denn es kann gut sein, dass du auf Systeme triffst, die weiterhin mit ihnen laufen. Ein Grund für die anhaltende Beliebtheit von .NET Framework ist, dass es eine Handvoll Dinge kann, die .NET 6.0 nicht kann. .NET Framework läuft nur unter Windows, während .NET 6.0 Windows, macOS und Linux unterstützt. Das macht .NET Framework zwar weniger weit verbreitet, bedeutet aber, dass es einige Windows-spezifische Funktionen unterstützen kann. So gibt es zum Beispiel einen Abschnitt der .NET Framework-Klassenbibliothek, der für die Arbeit mit COM+ Component Services vorgesehen ist, einer Windows-Funktion zum Hosten von Komponenten, die in den Microsoft Transaction Server integriert sind. Das ist bei den neueren, plattformübergreifenden Versionen von .NET nicht möglich, weil der Code möglicherweise unter Linux läuft, wo es entsprechende Funktionen entweder nicht gibt oder sie zu unterschiedlich sind, um über dieselbe .NET-API dargestellt zu werden.

Die Zahl der Funktionen, die nur für das .NET-Framework verfügbar sind, ist in den letzten Versionen drastisch gesunken, weil Microsoft daran gearbeitet hat, dass auch reine Windows-Anwendungen die neueste Version von .NET 6.0 nutzen können. Zum Beispiel war die System.Speech.NET-Bibliothek früher nur auf dem .NET-Framework verfügbar, weil sie Zugang zu Windows-spezifischen Spracherkennungs- und -synthesefunktionen bietet, aber jetzt gibt es eine .NET 6.0-Version dieser Bibliothek. Diese Bibliothek funktioniert nur unter Windows, aber ihre Verfügbarkeit bedeutet, dass Anwendungsentwickler, die sich auf sie verlassen, jetzt von .NET Framework auf .NET umsteigen können. Die übrigen Funktionen des .NET Framework, die nicht weiterentwickelt wurden, werden nicht häufig genug genutzt, um den Entwicklungsaufwand zu rechtfertigen. Die Unterstützung von COM+ war nicht nur eine Bibliothek, sondern hatte auch Auswirkungen auf die Art und Weise, wie die CLR Code ausführt, so dass die Unterstützung dieser Funktion in modernem .NET Kosten verursacht hätte, die für eine selten genutzte Funktion nicht gerechtfertigt gewesen wären.

Das plattformübergreifende .NET ist der Ort, an dem in den letzten Jahren die meisten neuen Entwicklungen von .NET stattgefunden haben. .NET Framework wird zwar noch unterstützt, ist aber seit einiger Zeit ins Hintertreffen geraten. Microsofts Webapplikations-Framework ASP.NET Core zum Beispiel unterstützt .NET Framework seit 2019 nicht mehr. Die Ausmusterung von .NET Framework und die Einführung von .NET 6.0 als das einzig wahre .NET ist also der unvermeidliche Abschluss eines Prozesses, der schon seit einigen Jahren im Gange ist.

Release-Zyklen und langfristiger Support

Microsoft veröffentlicht derzeit jedes Jahr eine neue Version von .NET, normalerweise im November oder Dezember, aber nicht alle Versionen sind gleich. Wechselnde Versionen erhalten Long Term Support (LTS), was bedeutet, dass Microsoft sich verpflichtet, die Version mindestens drei Jahre lang zu unterstützen. Während dieses Zeitraums werden die Tools, Bibliotheken und die Laufzeitumgebung regelmäßig mit Sicherheitspatches aktualisiert. .NET 6.0, das im November 2021 veröffentlicht wird, ist eine LTS-Version. Die vorangegangene LTS-Version war .NET Core 3.1, die im Dezember 2019 veröffentlicht wurde und daher noch bis Dezember 2022 unterstützt wird; die LTS-Version davor war .NET Core 2.1, die im August 2021 aus dem Support ging.3

Was ist mit Nicht-LTS-Versionen? Diese werden ab der Veröffentlichung unterstützt, aber sechs Monate nach dem Erscheinen der nächsten LTS-Version wird der Support eingestellt. Zum Beispiel wurde .NET 5.0 unterstützt, als es im Dezember 2020 veröffentlicht wurde, aber der Support endete im Mai 2022, sechs Monate nachdem .NET 6.0 veröffentlicht wurde. Microsoft kann den Support natürlich verlängern, aber für Planungszwecke ist es ratsam, davon auszugehen, dass Nicht-LTS-Versionen in etwa 18 Monaten unbrauchbar werden.

Es dauert oft ein paar Monate, bis das Ökosystem mit einer neuen Version mithalten kann. Es kann sein, dass du eine neue Version von .NET am Tag ihrer Veröffentlichung in der Praxis nicht nutzen kannst, weil dein Cloud-Provider sie noch nicht unterstützt oder weil es Inkompatibilitäten mit Bibliotheken gibt, die du nutzen musst. Das verkürzt die effektive Nutzungsdauer von Nicht-LTS-Versionen erheblich und kann dazu führen, dass dir ein unangenehm kleines Zeitfenster für ein Upgrade bleibt, wenn die nächste LTS-Version erscheint. Wenn es ein paar Monate dauert, bis die Tools, Plattformen und Bibliotheken, auf die du angewiesen bist, an die neue Version angepasst sind, bleibt dir nur wenig Zeit, um aufzusteigen, bevor der Support ausläuft. In extremen Situationen kann es sein, dass dieses Zeitfenster gar nicht existiert: .NET Core 2.2 erreichte das Ende des Supports, bevor Azure Functions vollen Support für .NET Core 3.0 oder 3.1 anbot. Entwickler, die die nicht-LTS-Version von .NET Core 2.2 für Azure Functions verwendet hatten, fanden sich also in einer Situation wieder, in der die letzte unterstützte Version tatsächlich rückwärts lief: Sie mussten sich entscheiden, entweder auf .NET Core 2.1 zurückzusteigen oder für ein paar Monate eine nicht unterstützte Runtime in der Produktion zu verwenden. Aus diesem Grund betrachten einige Entwickler die Nicht-LTS-Versionen als Vorabversionen - sie können neue Funktionen ausprobieren, um sie in der Produktion zu verwenden, sobald sie in einer LTS-Version erscheinen.

Mehrere .NET-Versionen mit .NET Standard anvisieren

Die Vielzahl der Laufzeiten, jede mit ihrer eigenen Version der Laufzeitbibliotheken, war lange Zeit eine Herausforderung für alle, die ihren C#-Code anderen Entwicklern zur Verfügung stellen wollten. Obwohl die Konvergenz, die wir mit .NET 6.0 endlich sehen, dieses Problem verringern kann, wird es üblich sein, weiterhin Systeme zu unterstützen, die mit dem alten .NET Framework laufen. Das bedeutet, dass es auf absehbare Zeit sinnvoll sein wird, Komponenten zu entwickeln, die auf mehrere .NET-Laufzeitsysteme abzielen. Es gibt ein Paket-Repository für .NET-Komponenten, in dem Microsoft alle .NET-Bibliotheken veröffentlicht, die nicht in .NET selbst integriert sind, und in dem auch die meisten .NET-Entwickler die Bibliotheken veröffentlichen, die sie weitergeben möchten. Aber für welche Version solltest du bauen? Das ist eine zweidimensionale Frage: Es gibt die Laufzeitimplementierung (.NET, .NET Framework) und auch die Version (zum Beispiel .NET Core 3.1 oder .NET 6.0; .NET Framework 4.7.2 oder 4.8). Viele Autoren von beliebten Open-Source-Paketen, die über NuGet vertrieben werden, unterstützen eine Vielzahl von alten und neuen Versionen.

Komponentenautoren haben oft mehrere Laufzeiten unterstützt, indem sie mehrere Varianten ihrer Bibliotheken erstellt haben. Wenn du .NET-Bibliotheken über NuGet verteilst, kannst du mehrere Sätze von Binärdateien in das Paket einbetten, die jeweils auf verschiedene Varianten von .NET ausgerichtet sind. Ein großes Problem dabei ist jedoch, dass mit den neuen .NET-Versionen, die im Laufe der Jahre erschienen sind, die vorhandenen Bibliotheken nicht auf allen neueren Laufwerken laufen. Eine Komponente, die für .NET Framework 4.0 geschrieben wurde, würde auf allen nachfolgenden Versionen von .NET Framework funktionieren, aber nicht auf .NET 6.0. Selbst wenn der Quellcode der Komponente vollständig mit der neueren Laufzeitumgebung kompatibel wäre, bräuchtest du eine separate Version, die für diese Plattform kompiliert wird. Und wenn der Autor einer Bibliothek, die du verwendest, keine explizite Unterstützung für .NET,4 konntest du sie nicht verwenden. Das war schlecht für alle. Im Laufe der Jahre sind verschiedene Versionen von .NET aufgetaucht und wieder verschwunden (z. B. Silverlight und verschiedene Windows Phone-Varianten), was bedeutete, dass die Autoren von Komponenten immer wieder neue Varianten ihrer Komponenten herausbringen mussten, und da dies davon abhängt, dass die Autoren die Lust und die Zeit haben, diese Arbeit zu erledigen, konnten die Konsumenten von Komponenten feststellen, dass nicht alle Komponenten, die sie verwenden wollten, auf der von ihnen gewählten Plattform verfügbar waren.

Um dies zu vermeiden, hat Microsoft den .NET Standard eingeführt, der gemeinsame Teilmengen der API-Fläche der .NET-Laufzeitbibliotheken definiert. Wenn ein NuGet-Paket z. B. auf .NET Standard 1.0 abzielt, garantiert dies, dass es auf den Versionen .NET Framework 4.5 oder höher, .NET Core 1.0 oder höher, .NET 5.0 und höher oder Mono 4.6 oder höher laufen kann. Und wenn eine weitere Variante von .NET auftaucht, können die vorhandenen Komponenten ohne Änderungen ausgeführt werden, solange sie den .NET Standard 1.0 unterstützen, auch wenn die neue Plattform noch nicht existierte, als sie geschrieben wurden.

Heute ist der .NET Standard 2.0 wahrscheinlich die beste Wahl für Komponentenautoren, die eine breite Palette von Plattformen unterstützen wollen, weil alle kürzlich veröffentlichten Versionen von .NET ihn unterstützen und er Zugang zu einer sehr breiten Palette von Funktionen bietet. Allerdings ist die Anzahl der verschiedenen Varianten von .NET, die Microsoft heute noch unterstützt, viel geringer als zur Zeit der Einführung von .NET Standard, so dass der .NET Standard wohl weniger wichtig ist als früher. Heute besteht der Hauptvorteil von .NET Standard darin, dass dein Code sowohl auf .NET Framework als auch auf .NET Core und .NET läuft. Wenn du das .NET Framework nicht unterstützen musst, ist es sinnvoller, stattdessen auf .NET Core 3.1 oder .NET 6.0 zu setzen. In Kapitel 12 werden einige Überlegungen zum .NET Standard genauer beschrieben.

Microsoft bietet mehr als nur eine Sprache und die verschiedenen Laufzeiten mit den dazugehörigen Klassenbibliotheken. Es gibt auch Entwicklungsumgebungen, die dir beim Schreiben, Testen, Debuggen und Warten deines Codes helfen.

Visual Studio, Visual Studio Code und JetBrains Rider

Microsoft bietet drei Desktop-Entwicklungsumgebungen an: Visual Studio Code, Visual Studio und Visual Studio für Mac. Alle drei bieten die grundlegenden Funktionen - wie einen Texteditor, Build-Tools und einen Debugger - aber Visual Studio bietet die umfangreichste Unterstützung für die Entwicklung von C#-Anwendungen, unabhängig davon, ob diese Anwendungen auf Windows oder anderen Plattformen laufen sollen. Es ist am längsten auf dem Markt - genauso lange wie C# - und stammt somit aus der Zeit vor Open Source und ist nach wie vor ein Closed-Source-Produkt. Die verschiedenen verfügbaren Editionen reichen von kostenlos bis sehr teuer. Microsoft ist nicht die einzige Option: Das Unternehmen JetBrains verkauft eine vollwertige .NET-IDE namens Rider, die auf Windows, Linux und macOS läuft.

Visual Studio ist eine integrierte Entwicklungsumgebung (Integrated Development Environment, IDE) und verfolgt daher den Ansatz "alles inklusive". Zusätzlich zu einem vollwertigen Texteditor bietet es visuelle Bearbeitungswerkzeuge für Benutzeroberflächen. Es besteht eine tiefe Integration mit Versionskontrollsystemen wie Git und mit Online-Systemen, die Quellcode-Repositories, Problemverfolgung und andere Funktionen für das Application Lifecycle Management (ALM) bereitstellen, z. B. GitHub und das Azure DevOps-System von Microsoft. Visual Studio bietet integrierte Überwachungs- und Diagnose-Tools. Es verfügt über verschiedene Funktionen für die Arbeit mit Anwendungen, die für die Azure-Cloud-Plattform von Microsoft entwickelt und dort bereitgestellt wurden. Von den drei hier beschriebenen Microsoft-Umgebungen verfügt es über die umfangreichsten Refactoring-Funktionen. Beachte, dass Visual Studio nur unter Windows läuft.

2017 hat Microsoft Visual Studio für Mac veröffentlicht. Dabei handelt es sich nicht um eine Portierung der Windows-Version. Es ist aus einer Plattform namens Xamarin hervorgegangen, einer Mac-basierten Entwicklungsumgebung, die sich auf die Erstellung von mobilen Apps in C# spezialisiert hat, die auf der Mono-Laufzeitumgebung laufen. Ursprünglich war Xamarin eine unabhängige Technologie, aber als Microsoft, wie bereits erwähnt, das Unternehmen aufkaufte, das sie entwickelte, integrierte Microsoft verschiedene Funktionen aus der Windows-Version von Visual Studio, als es das Produkt unter der Marke Visual Studio einführte.

Die JetBrains Rider IDE ist ein einzelnes Produkt, das auf drei Betriebssystemen läuft. Sie ist konzentrierter als Visual Studio, da sie ausschließlich für die Entwicklung von .NET-Anwendungen entwickelt wurde. (Visual Studio unterstützt auch C++.) Es hat einen ähnlichen "Alles-inklusive"-Ansatz und bietet eine besonders leistungsstarke Auswahl an Refactoring-Tools.

Visual Studio Code (oft mit VS Code abgekürzt) wurde erstmals 2015 veröffentlicht. Es ist Open Source und plattformübergreifend und unterstützt sowohl Linux als auch Windows und Mac. Es basiert auf der Electron-Plattform und ist überwiegend in TypeScript geschrieben. (Das bedeutet, dass VS Code im Gegensatz zu Visual Studio auf allen Betriebssystemen wirklich dasselbe Programm ist). VS Code ist ein schlankeres Produkt als Visual Studio: Die Grundinstallation von VS Code bietet nicht viel mehr als Unterstützung für die Textbearbeitung. Wenn du jedoch Dateien öffnest, entdeckt es herunterladbare Erweiterungen, die, wenn du sie installieren möchtest, Unterstützung für C#, F#, TypeScript, PowerShell, Python und eine Vielzahl anderer Sprachen bieten. (Der Erweiterungsmechanismus ist offen, sodass jeder, der möchte, eine Erweiterung veröffentlichen kann). Obwohl sie in ihrer ursprünglichen Form weniger eine IDE als vielmehr ein einfacher Texteditor ist, ist sie dank ihres Erweiterungsmodells ziemlich leistungsstark. Die große Auswahl an Erweiterungen hat dazu geführt, dass VS Code auch außerhalb der Microsoft-Sprachwelt sehr beliebt ist, was wiederum einen positiven Kreislauf in Gang gesetzt hat, in dem die Zahl der Erweiterungen noch weiter wächst.

Visual Studio und JetBrains Rider bieten den einfachsten Weg, um in C# einzusteigen - du musst keine Erweiterungen installieren oder die Konfiguration ändern, um loslegen zu können. Visual Studio Code ist jedoch für ein breiteres Publikum verfügbar, daher werde ich es in der folgenden kurzen Einführung in die Arbeit mit C# verwenden. Die grundlegenden Konzepte gelten jedoch für alle Umgebungen. Wenn du also Visual Studio oder Rider verwendest, gilt das meiste von dem, was ich hier beschreibe, auch.

Tipp

Du kannst Visual Studio Code kostenlos herunterladen. Außerdem musst du das .NET SDK installieren.

Wenn du mit Windows arbeitest und lieber Visual Studio verwenden möchtest, kannst du die kostenlose Version von Visual Studio herunterladen, die Visual Studio Community heißt. Damit wird das .NET SDK für dich installiert, solange du bei der Installation mindestens einen . NET-Workload auswählst.

Jedes nicht-triviale C#-Projekt hat mehrere Quellcode-Dateien, die zu einem Projekt gehören. Jedes Projekt erstellt eine einzige Ausgabe, das sogenannte Ziel. Das Build-Ziel kann eine einfache Datei sein - ein C#-Projekt kann z. B. eine ausführbare Datei oder eine Bibliothek erzeugen - aber manche Projekte erzeugen auch kompliziertere Ausgaben. Einige Projekttypen erstellen zum Beispiel Websites. Eine Website enthält in der Regel mehrere Dateien, die aber zusammen eine einzige Einheit bilden: eine Website. Die Ergebnisse jedes Projekts werden als eine Einheit bereitgestellt, auch wenn sie aus mehreren Dateien bestehen.

Hinweis

Ausführbare Dateien haben unter Windows normalerweise die Dateierweiterung .exe, während Bibliotheken die Endung .dll (historisch gesehen die Abkürzung für Dynamic Link Library) haben. Bei .NET wird jedoch der gesamte Code in .dll-Dateien gespeichert. Das SDK kann auch eine ausführbare Bootstrapping-Datei (mit der Dateierweiterung .exe unter Windows) erzeugen, aber diese startet nur die Runtime und lädt dann die .dll, die die wichtigste kompilierte Ausgabe enthält. (Etwas anders sieht es aus, wenn du das .NET Framework verwendest: Dann wird die Anwendung direkt in eine selbst-bootstrapping .exe kompiliert, ohne separate .dll). Der einzige Unterschied zwischen der kompilierten Hauptausgabe einer Anwendung und einer Bibliothek besteht darin, dass erstere einen Einstiegspunkt für die Anwendung angibt. Beide Dateitypen können Funktionen exportieren, die von anderen Komponenten genutzt werden können. Dies sind beides Beispiele für Assemblies, die in Kapitel 12 behandelt werden.

C#-Projektdateien haben die Erweiterung .csproj. Wenn du diese Dateien mit einem Texteditor untersuchst, wirst du feststellen, dass sie XML enthalten. Eine .csproj-Datei beschreibt den Inhalt des Projekts und legt fest, wie es erstellt werden soll. Diese Dateien werden sowohl von Visual Studio als auch von den .NET-Erweiterungen für VS Code erkannt. Sie werden auch von verschiedenen Befehlszeilen-Build-Tools verstanden, wie z. B. dem dotnet Befehlszeilen-Tool, das mit dem .NET SDK installiert wird, und auch von Microsofts älterem MSBuild-Tool. (MSBuild unterstützt zahlreiche Sprachen und Ziele, nicht nur .NET. Wenn du ein C#-Projekt mit dem Befehl dotnet build des .NET SDK erstellst, ist das praktisch ein Wrapper für MSBuild).

Du wirst oft mit Gruppen von Projekten arbeiten wollen. So ist es zum Beispiel eine gute Praxis, Tests für deinen Code zu schreiben, aber der meiste Testcode muss nicht als Teil der Anwendung eingesetzt werden, sodass du automatisierte Tests in der Regel in separate Projekte einbaust. Vielleicht möchtest du deinen Code auch aus anderen Gründen aufteilen. Vielleicht besteht das System, das du aufbaust, aus einer Desktop-Anwendung und einer Website, und du hast gemeinsamen Code, den du in beiden Anwendungen verwenden möchtest. In diesem Fall bräuchtest du ein Projekt, das eine Bibliothek mit dem gemeinsamen Code erstellt, ein weiteres, das die ausführbare Datei für die Desktop-Anwendung erzeugt, ein weiteres, das die Website erstellt, und drei weitere Projekte, die die Tests für jedes der Hauptprojekte enthalten.

Die Build-Tools und IDEs, die .NET verstehen, helfen dir dabei, mit mehreren zusammenhängenden Projekten zu arbeiten, die sie Solution nennen. Eine Solution ist eine Datei mit der Erweiterung .sln, die eine Sammlung von Projekten definiert. Die Projekte in einer Solution sind normalerweise miteinander verbunden, müssen es aber nicht sein.

Wenn du Visual Studio verwendest, solltest du wissen, dass Projekte zu einer Lösung gehören müssen, auch wenn du nur ein Projekt hast. Visual Studio Code kann zwar auch ein einzelnes Projekt öffnen, aber die .NET-Erweiterungen erkennen auch Lösungen.

Ein Projekt kann zu mehr als einer Lösung gehören. Bei einer großen Codebasis ist es üblich, dass es mehrere .sln-Dateien mit verschiedenen Kombinationen von Projekten gibt. Normalerweise gibt es eine Hauptlösung, die jedes einzelne Projekt enthält, aber nicht alle Entwickler/innen wollen ständig mit dem gesamten Code arbeiten. Jemand, der in unserem hypothetischen Beispiel an der Desktop-Anwendung arbeitet, wird auch die gemeinsam genutzte Bibliothek benötigen, hat aber wahrscheinlich kein Interesse daran, das Webprojekt zu laden.

Ich zeige dir, wie du ein neues Projekt erstellst, es in Visual Studio Code öffnest und es ausführst. Dann gehe ich die verschiedenen Funktionen eines neuen C#-Projekts durch, um dir die Sprache näher zu bringen. Außerdem zeige ich dir, wie du ein Unit-Test-Projekt hinzufügst und wie du eine Lösung erstellst, die beides enthält.

Anatomie eines einfachen Programms

Sobald du das .NET 6.0 SDK entweder direkt oder durch die Installation einer IDE installiert hast, kannst du ein neues .NET-Programm erstellen. Beginne damit, auf deinem Computer ein neues Verzeichnis mit dem Namen HelloWorld anzulegen, in dem der Code gespeichert wird. Öffne eine Eingabeaufforderung und vergewissere dich, dass das aktuelle Verzeichnis so eingestellt ist, und führe dann diesen Befehl aus:

dotnet new console

Damit wird eine neue C#-Konsolenanwendung erstellt, indem zwei Dateien angelegt werden. Es wird eine Projektdatei mit einem Namen erstellt, der auf dem übergeordneten Verzeichnis basiert: In diesem Fall HelloWorld.csproj. Außerdem gibt es eine Datei Program.cs, die den Code enthält. Wenn du diese Datei in einem Texteditor öffnest, wirst du feststellen, dass sie ziemlich einfach ist, wie Beispiel 1-1 zeigt.

Beispiel 1-1. Unser erstes Programm
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

Du kannst dieses Programm mit dem folgenden Befehl kompilieren und ausführen:

dotnet run

Wie du wahrscheinlich schon vermutet hast, wird der Text Hello, World! als Ausgabe angezeigt.

Wenn du bereits etwas Erfahrung mit C# hast und dieses Buch liest, um zu erfahren, was in C# 10.0 neu ist, wird dich dieses Beispiel vielleicht ein wenig überraschen. In früheren Versionen der Sprache war das klassische "Hello, World!"-Beispiel, mit dem alle Programmierbücher laut Gesetz beginnen müssen, deutlich größer. Dieses Beispiel sieht so anders aus, dass die Autoren des .NET SDK es für nötig hielten, eine Erklärung abzugeben - mehr als die Hälfte dieses Beispiels besteht nur aus einem Kommentar mit einem Link zu einer Webseite, auf der erklärt wird, wo der Rest des Codes geblieben ist. Die zweite Zeile hier ist alles, was du brauchst.

Dies verdeutlicht eine der Änderungen, die C# 10.0 einführt: Es soll Anwendungen ermöglichen, direkt auf den Punkt zu kommen, indem die Menge an Boilerplate reduziert wird. Als Boilerplate wird Code bezeichnet, der zur Einhaltung bestimmter Regeln oder Konventionen erforderlich ist, aber in jedem Projekt mehr oder weniger gleich aussieht. In C# muss der Code zum Beispiel innerhalb einer Methode definiert werden, und eine Methode muss immer innerhalb eines Typs definiert werden. Du kannst diese Regeln in Beispiel 1-1 sehen. Um eine Ausgabe zu erzeugen, verlässt es sich auf die Fähigkeit der .NET-Laufzeitumgebung, Text anzuzeigen, die in einer Methode namens WriteLine verkörpert ist. Aber wir sagen nicht einfach WriteLine, weil C#-Methoden immer zu Typen gehören, weshalb der Code dies als Console.WriteLine bezeichnet.

Jeder C#-Code, den wir schreiben, unterliegt natürlich diesen Regeln. Unser Code, der die Methode Console.WriteLine aufruft, muss also selbst innerhalb einer Methode innerhalb eines Typs stehen. Und in den meisten C#-Codes ist das explizit: In den meisten Fällen siehst du etwas wie in Beispiel 1-2.

Beispiel 1-2. "Hallo, Welt!" mit sichtbarem Boilerplate
using System;

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

Hier gibt es nur noch eine Zeile, die das Verhalten der Anwendung definiert, und das ist dieselbe wie in Beispiel 1-1. Der offensichtliche Vorteil des ersten Beispiels ist, dass wir uns auf das konzentrieren können, was unser Programm tatsächlich tut, auch wenn der Nachteil darin besteht, dass ziemlich viel unsichtbar wird. Mit dem expliziten Stil in Beispiel 1-2 wird nichts versteckt. In Beispiel 1-1 platziert der Compiler den Code immer noch in einer Methode, die in einem Typ namens Program definiert ist, nur dass man das im Code nicht sehen kann. In Beispiel 1-2 sind die Methode und der Typ deutlich sichtbar.

In der Praxis sieht der meiste C#-Code eher wie Beispiel 1-2 als wie Beispiel 1-1 aus, denn einige der Maßnahmen zur Reduzierung von Boilerplate in C# 10.0 beziehen sich nur auf den Einstiegspunkt des Programms. Wenn du den Code schreibst, der beim Start deines Programms ausgeführt werden soll, musst du keine Klasse oder Methode definieren, die das Programm enthält. Aber ein Programm hat nur einen Einstiegspunkt, und für alles andere musst du ihn immer noch angeben.

Da echte Projekte mehrere Dateien und in der Regel auch mehrere Projekte umfassen, wollen wir uns einem etwas realistischeren Beispiel zuwenden. Ich werde ein Programm erstellen, das den Durchschnitt (das arithmetische Mittel, um genau zu sein) von einigen Zahlen berechnet. Außerdem werde ich ein zweites Projekt erstellen, das automatisch unser erstes Projekt testet. Da ich zwei Projekte habe, brauche ich dieses Mal eine Lösung. Ich werde ein neues Verzeichnis namens Averages erstellen. Wenn du mir folgst, ist es egal, wo es liegt, aber es ist eine gute Idee, es nicht in das Verzeichnis deines ersten Projekts zu legen. Ich öffne eine Eingabeaufforderung in diesem Verzeichnis und führe diesen Befehl aus:

dotnet new sln

Dadurch wird eine neue Lösungsdatei mit dem Namen Averages.sln erstellt. (Standardmäßig benennt dotnet new neue Projekte und Lösungen nach den Verzeichnissen, in denen sie sich befinden, du kannst aber auch andere Namen angeben). Jetzt füge ich mit diesen beiden Befehlen die beiden Projekte hinzu, die ich brauche:

dotnet new console -o Averages
dotnet new mstest -o Averages.Tests

Die Option -o (kurz für output) zeigt an, dass jedes dieser neuen Projekte in einem neuen Unterverzeichnis erstellt werden soll - wenn du mehrere Projekte hast, braucht jedes sein eigenes Verzeichnis.

Diese muss ich nun der Lösung hinzufügen:

dotnet sln add ./Averages/Averages.csproj
dotnet sln add ./Averages.Tests/Averages.Tests.csproj

In diesem zweiten Projekt werde ich einige Tests definieren, die den Code im ersten Projekt überprüfen (deshalb habe ich den Projekttyp mstestangegeben - dieses Projekt wird das Unit-Test-Framework von Microsoft verwenden). Damit das funktioniert, muss das zweite Projekt Zugriff auf den Code des ersten Projekts haben. Um das zu ermöglichen, führe ich diesen Befehl aus:

dotnet add ./Averages.Tests/Averages.Tests.csproj reference
./Averages/Averages.csproj

(Ich habe das auf zwei Zeilen aufgeteilt, damit es passt, aber es muss als ein einziger Befehl ausgeführt werden). Um das Projekt zu bearbeiten, kann ich VS Code im aktuellen Verzeichnis mit diesem Befehl starten:

code .

Wenn du mitkommst und VS Code zum ersten Mal startest, wirst du aufgefordert, einige Entscheidungen zu treffen, z. B. ein Farbschema auszuwählen. Du könntest versucht sein, die Fragen zu ignorieren, aber an dieser Stelle wird dir u. a. angeboten, Erweiterungen für die Sprachunterstützung zu installieren. VS Code wird mit allen möglichen Sprachen verwendet, und das Installationsprogramm macht keine Annahmen darüber, welche Sprache du verwenden wirst, also musst du eine Erweiterung installieren, um C#-Unterstützung zu erhalten. Wenn du aber den Anweisungen von VS Code folgst, um nach Spracherweiterungen zu suchen, wird dir die C#-Erweiterung von Microsoft angeboten. Kein Grund zur Panik, wenn VS Code dies nicht anbietet. Vielleicht hast du die Erweiterung bereits installiert und sie stellt dir nicht mehr diese einleitenden Fragen, oder das Verhalten von VS Code beim ersten Start hat sich geändert, seit ich diesen Artikel geschrieben habe. Du kannst die Erweiterung immer noch ganz einfach finden. Klicke auf das Erweiterungssymbol in der Leiste auf der linken Seite und es wird eine Reihe von Erweiterungen angezeigt, die für dich relevant sein könnten. Wenn du VS-Code in einem Verzeichnis geöffnet hast, in dem sich eine .csproj-Datei befindet, wird die C#-Erweiterung angezeigt. Und wenn alles andere fehlschlägt, kannst du nach den Erweiterungen suchen, die du brauchst. Abbildung 1-1 zeigt das Erweiterungs-Panel von VS Code - du kannst es aufrufen, indem du auf das Symbol in der Leiste auf der linken Seite klickst. Es ist das Symbol mit den vier Quadraten, das hier unten zu sehen ist.

Visual Studio Code's C# Extension
Abbildung 1-1. Die C#-Erweiterung von Visual Studio Code

Wie du siehst, habe ich C# in das Suchfeld oben eingegeben, und das erste Ergebnis ist die C#-Erweiterung von Microsoft. Es werden auch noch ein paar andere Ergebnisse angezeigt. Vergewissere dich, dass du das richtige Ergebnis erwischst, wenn du der Suche folgst. Wenn du auf das Suchergebnis klickst, werden detailliertere Informationen angezeigt. Der vollständige Name lautet "C# for Visual Studio Code (powered by OmniSharp)" und als Herausgeber wird "Microsoft" angegeben. Klicke auf die Schaltfläche Installieren, um die Erweiterung zu installieren.

Es kann ein paar Minuten dauern, bis die C#-Erweiterung heruntergeladen und installiert ist. Sobald das erledigt ist, sollte die Statusleiste unten links im Fenster ähnlich wie in Abbildung 1-2 aussehen und den Namen der Lösungsdatei sowie ein Flammensymbol anzeigen, das darauf hinweist, dass OmniSharp, das System, das C#-Unterstützung in VS Code bietet, bereit ist. Es ist möglich, dass oben im Fenster eine Projektauswahl erscheint - die C#-Erweiterung hat das Lösungsverzeichnis durchsucht und die beiden C#-Projekte sowie die zugehörige Lösung gefunden. Normalerweise wird nur die Lösungsdatei geöffnet, aber je nachdem, wie dein System konfiguriert ist, kann es sein, dass du gefragt wirst, welche du verwenden möchtest. Ich werde mit beiden Projekten in der Projektmappe arbeiten, also wähle ich den Eintrag Averages.sln.

Visual Studio Code's status bar showing the OmniSharp icon and the name of the solution file
Abbildung 1-2. Visual Studio Code Statusleiste

Die C#-Erweiterung wird jetzt den gesamten Quellcode in allen Projekten der Lösung untersuchen. Natürlich ist darin noch nicht viel enthalten, aber sie wird den Code weiter analysieren, während ich tippe, damit sie Probleme erkennen und hilfreiche Vorschläge machen kann. Während dieses Prozesses wird es feststellen, dass es noch keine Konfiguration für das Erstellen und Debuggen der Projekte gibt. Unten rechts im Fenster wird ein Dialog angezeigt, in dem du diese hinzufügen kannst (siehe Abbildung 1-3 ). Es ist ratsam, auf die Schaltfläche Ja zu klicken und bei der Frage, welches Projekt gestartet werden soll, das Hauptprogramm Averages.csproj auszuwählen, damit VS Code weiß, welches Projekt er verwenden soll, wenn er zum Ausführen oder Debuggen des Codes aufgefordert wird.

A dialog with this text: Required assets to build and debug are missing from Averages. Add them?
Abbildung 1-3. C#-Erweiterung zum Hinzufügen von Build- und Debug-Assets

Ich kann einen Blick auf den Code werfen, indem ich zur Explorer-Ansicht wechsle, indem ich auf die Schaltfläche oben in der linken Symbolleiste klicke. Wie Abbildung 1-4 zeigt, werden die Verzeichnisse und Dateien angezeigt. Ich habe das Verzeichnis Averages.Test erweitert und die DateiUnitTest1.cs ausgewählt.

Visual Studio Code's Explorer, with the Averages.Test project expanded, and the UnitTest1.cs file selected
Abbildung 1-4. Der Explorer von Visual Studio Code
Tipp

Wenn du im Explorer auf eine Datei klickst, zeigt VS Code sie in einer Vorschauregisterkarte an. Das bedeutet, dass sie nicht lange geöffnet bleibt: Sobald du auf eine andere Datei klickst, verdrängt diese die zuvor geöffnete. Damit soll vermieden werden, dass du am Ende Hunderte von offenen Tabs hast, aber wenn du zwischen zwei Dateien hin und her arbeitest, kann das nervig sein. Du kannst dies vermeiden, indem du beim Öffnen einer Datei auf diese doppelklickst - dadurch wird eine Registerkarte geöffnet, die nicht zur Voransicht gehört. Wenn du eine Datei bereits in einer Vorschauregisterkarte geöffnet hast, kannst du auch auf die Registerkarte doppelklicken, um sie in eine normale Registerkarte zu verwandeln. VS Code zeigt den Dateinamen in Vorschauregisterkarten kursiv an. Wenn du darauf doppelklickst, wird er nicht mehr kursiv angezeigt.

Du fragst dich vielleicht, warum ich das Verzeichnis Averages.Tests erweitert habe. Der Zweck dieses Testprojekts ist es, sicherzustellen, dass das Hauptprojekt das tut, was es soll. Ich bevorzuge den Entwicklungsstil, bei dem du deine Tests schreibst, bevor du den zu testenden Code schreibst, also fange ich mit dem Testprojekt an.

Einen Einheitstest schreiben

Als ich vorhin den Befehl zum Erstellen dieses Projekts ausgeführt habe, habe ich den Projekttyp mstest angegeben. Diese Projektvorlage hat mir eine Testklasse zur Verfügung gestellt, die ich in einer Datei namens UnitTest1.cs speichern kann. Ich möchte einen aussagekräftigeren Namen wählen. Es gibt verschiedene Ansichten darüber, wie du deine Unit-Tests strukturieren solltest. Manche Entwickler befürworten eine Testklasse für jede Klasse, die du testen willst. Ich hingegen bevorzuge den Stil, bei dem du für jedes Szenario, in dem du eine bestimmte Klasse testen willst, eine Klasse schreibst, mit einer Methode für jedes der Dinge, die in diesem Szenario an deinem Code wahr sein sollen. Dieses Programm wird nur ein Verhalten haben: Es wird das arithmetische Mittel seiner Eingaben berechnen. Ich benenne also die Quelldatei UnitTest1.cs in WhenCalculatingAverages.cs um. (Du kannst eine Datei umbenennen, indem du mit der rechten Maustaste im Explorer von VS Code auf sie klickst und den Eintrag Umbenennen auswählst). Dieser Test soll sicherstellen, dass wir für einige repräsentative Eingaben die erwarteten Ergebnisse erhalten. Beispiel 1-3 zeigt eine vollständige Quelldatei, die dies tut; hier gibt es zwei Tests, die fett gedruckt sind.

Beispiel 1-3. Eine Unit-Test-Klasse für unser erstes Programm
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Averages.Tests;

[TestClass]
public class WhenCalculatingAverages
{
    [TestMethod]
    public void SingleInputShouldProduceSameValueAsResult()
    {
        string[] inputs = { "1" };
        double result = AverageCalculator.ArithmeticMean(inputs);
        Assert.AreEqual(1.0, result, 1E-14);
    }

    [TestMethod]
    public void MultipleInputsShouldProduceAverageAsResult()
    {
        string[] inputs = { "1", "2", "3" };
        double result = AverageCalculator.ArithmeticMean(inputs);
        Assert.AreEqual(2.0, result, 1E-14);
    }
}

Ich werde die einzelnen Funktionen in dieser Datei erklären, sobald ich das Programm selbst gezeigt habe. Im Moment sind die beiden Methoden die interessantesten Teile dieses Beispiels. Zuerst haben wir die Methode SingleInputShouldProduceSameValueAsResult, die überprüft, ob unser Programm den Fall einer einzelnen Eingabe richtig behandelt. Die erste Zeile in dieser Methode beschreibt die Eingabe - eine einzelne Zahl. (Etwas überraschend ist, dass dieser Test die Zahlen als Zeichenketten darstellt. Das liegt daran, dass unsere Eingaben letztendlich als Befehlszeilenargumente kommen, also muss unser Test das widerspiegeln.) Die zweite Zeile führt den zu testenden Code aus (den ich noch nicht geschrieben habe). Und die dritte Zeile besagt, dass der berechnete Durchschnitt gleich der einzigen Eingabe sein muss. Ist das nicht der Fall, meldet der Test einen Fehler. Die zweite Methode, MultipleInputsShouldProduceAverageAsResult, prüft einen etwas komplexeren Fall, in dem es drei Eingaben gibt, hat aber die gleiche Grundform wie die erste.

Hinweis

Wir arbeiten hier mit dem C#-Typ double, einer Fließkommazahl mit doppelter Genauigkeit, um mit Ergebnissen umgehen zu können, die keine ganzen Zahlen sind. Ich werde die eingebauten Datentypen von C# im nächsten Kapitel genauer beschreiben, aber sei dir bewusst, dass die Fließkomma-Arithmetik in C# wie in den meisten Programmiersprachen eine begrenzte Genauigkeit hat. Die Methode Assert.AreEqual, die ich hier zur Überprüfung der Ergebnisse verwende, berücksichtigt dies und lässt mich eine maximale Toleranz für Ungenauigkeiten angeben. Das letzte Argument von 1E-14 ist in jedem Fall die Zahl 1 geteilt durch 10 hoch 14. Diese Tests geben also an, dass die Antwort auf 14 Dezimalstellen genau sein muss.

Konzentrieren wir uns auf eine bestimmte Zeile aus diesen Tests: diejenige, die den Code ausführt, den ich testen möchte. Beispiel 1-4 zeigt die entsprechende Zeile aus Beispiel 1-3. So rufst du in C# eine Methode auf, die ein Ergebnis zurückgibt. Diese Zeile beginnt mit der Deklaration einer Variablen, die das Ergebnis aufnehmen soll. ( double steht für den Datentyp und result ist der Name der Variablen.) Alle Methoden in C# müssen innerhalb eines Typs definiert werden. Wie wir bereits im Beispiel Console.WriteLine gesehen haben, haben wir also auch hier die gleiche Form: ein Typname, dann ein Punkt, dann ein Methodenname. Und dann, in Klammern, die Eingabe für die Methode.

Beispiel 1-4. Aufrufen einer Methode
double result = AverageCalculator.ArithmeticMean(inputs);

Wenn du den Code während des Lesens eintippst, dann erstens: gut gemacht. Aber zweitens: Wenn du dir die beiden Stellen ansiehst, an denen diese Codezeile vorkommt (einmal in jeder Testmethode), wirst du feststellen, dass VS Code eine verschnörkelte Linie unter AverageCalculator gezeichnet hat. Wenn du mit der Maus über diese Linie fährst, wird eine Fehlermeldung angezeigt, wie in Abbildung 1-5 zu sehen ist.

Visual Studio Code showing the AverageCalculator symbol underlined, and an error popup containing this text: The name AverageCalculator does not exist in the current context Averages.Tests
Abbildung 1-5. Ein unerkannter Typ

Das sagt uns etwas, das wir schon wussten: Ich habe den Code, den dieser Test testen soll, noch nicht geschrieben. Lass uns das ändern. Ich muss eine neue Datei hinzufügen. Dazu klicke ich in der Explorer-Ansicht von VS Code auf das Verzeichnis " Averages" und dann auf die Schaltfläche ganz links in der Symbolleiste am oberen Rand des Explorers. Abbildung 1-6 zeigt, dass, wenn du mit der Maus über diese Schaltfläche fährst, ein Tooltip erscheint, der ihren Zweck bestätigt. Nachdem ich darauf geklickt habe, kann ich AverageCalculator.cs als Namen für die neue Datei eintippen.

Visual Studio Code's Explorer view, with the New File button highlighter, and a tooltip saying 'New File'
Abbildung 1-6. Hinzufügen einer neuen Datei

VS Code erstellt eine neue, leere Datei. Ich füge so wenig Code wie möglich ein, um den in Abbildung 1-5 gemeldeten Fehler zu beheben. Beispiel 1-5 wird den C#-Compiler zufriedenstellen. Es ist noch nicht vollständig - es führt nicht die notwendigen Berechnungen durch, aber dazu kommen wir noch.

Beispiel 1-5. Eine einfache Klasse
namespace Averages;

public static class AverageCalculator
{
    public static double ArithmeticMean(string[] args)
    {
        return 1.0;
    }
}

Da der Code jetzt kompiliert wird, kann ich die Tests mit diesem Befehl ausführen:

dotnet test

Dies ergibt die folgende Ausgabe:

  Failed MultipleInputsShouldProduceAverageAsResult [291 ms]
  Error Message:
   Assert.AreEqual failed. Expected a difference no greater than <1E-14>
 between expected value <2> and actual value <1>.
  Stack Trace:
     at Averages.Tests.WhenCalculatingAverages.
MultipleInputsShouldProduceAverageAsResult() in
C:\book\Averages\Averages.Tests\WhenCalculatingAverages.cs:line 21

Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2,
Duration: 364 ms - Averages.Tests.dll (net6.0)

Wie erwartet, gibt es Fehler, weil ich noch keine richtige Implementierung geschrieben habe. Aber zuerst möchte ich jedes Element von Beispiel 1-5 der Reihe nach erklären, da es eine nützliche Einführung in einige wichtige Elemente der C#-Syntax und -Struktur bietet. Das allererste Element in dieser Datei ist eine Namensraum-Deklaration.

Namensräume

Namensräume bringen Ordnung und Struktur in das, was sonst ein furchtbares Durcheinander wäre. Die .NET-Laufzeitbibliotheken enthalten eine große Anzahl von Typen, und es gibt noch viele weitere in Bibliotheken von Drittanbietern, ganz zu schweigen von den Klassen, die du selbst schreiben wirst. Es gibt zwei Probleme, die beim Umgang mit so vielen benannten Entitäten auftreten können. Erstens wird es schwierig, die Einzigartigkeit zu garantieren. Zweitens kann es schwierig werden, die API zu finden, die du brauchst. Wenn du den richtigen Namen nicht kennst oder erraten kannst, ist es schwierig, das, was du brauchst, in einer unstrukturierten Liste von Zehntausenden von Dingen zu finden. Namensräume lösen diese beiden Probleme.

Die meisten .NET-Typen sind in einem Namensraum definiert. Es gibt bestimmte Konventionen für Namensräume, die du oft sehen wirst. Die Typen in den .NET-Laufzeitbibliotheken befinden sich zum Beispiel in Namensräumen, die mit System beginnen. Außerdem hat Microsoft eine Vielzahl nützlicher Bibliotheken zur Verfügung gestellt, die nicht zum Kern von .NET gehören. Diese beginnen in der Regel mit Microsoft. Wenn sie nur für eine bestimmte Technologie bestimmt sind, können sie auch nach dieser benannt sein. (Zum Beispiel gibt es Bibliotheken für die Nutzung der Azure-Cloud-Plattform von Microsoft, die Typen in Namensräumen definieren, die mit Azure beginnen). Bibliotheken anderer Anbieter beginnen in der Regel mit dem Firmennamen oder einem Produktnamen, während Open-Source-Bibliotheken oft ihren Projektnamen verwenden. Du bist nicht gezwungen, deine eigenen Typen in Namensräume zu packen, aber es ist empfehlenswert, das zu tun. C# behandelt System nicht als speziellen Namensraum, also hält dich nichts davon ab, ihn für deine eigenen Typen zu verwenden. Wenn du aber keinen Beitrag zu den .NET-Laufzeitbibliotheken schreibst, den du als Pull-Request an das .NET-Laufzeit-Quellcode-Repository übermittelst, ist das eine schlechte Idee, weil es andere Entwickler verwirren kann. Für deinen eigenen Code solltest du etwas Unverwechselbares wählen, z. B. den Namen deines Unternehmens oder deines Projekts. Wie du in der ersten Zeile von Beispiel 1-5 sehen kannst, habe ich unsere Klasse AverageCalculator in einem Namensraum namens Averages definiert, der zu unserem Projektnamen passt.

Die Art der Namespace-Deklaration in Beispiel 1-5 ist neu in C# 10.0. Heutzutage wird der Großteil des Codes, dem du begegnest, wahrscheinlich den älteren, etwas ausführlicheren Stil aus Beispiel 1-6 verwenden. Der Unterschied besteht darin, dass die Namensraumdeklaration von geschweiften Klammern ({}) gefolgt wird und sich nur auf den Inhalt dieser Klammern bezieht. Dadurch ist es möglich, dass eine einzige Datei mehrere Namensraum-Deklarationen enthalten kann. In der Praxis enthalten die allermeisten C#-Dateien jedoch genau eine Namensraum-Deklaration. Mit der alten Syntax bedeutet das, dass der Großteil des Inhalts jeder Datei innerhalb eines Paars geschweifter Klammern stehen muss, eingerückt um einen Tabstopp. Der in Beispiel 1-5 gezeigte neue Stil gilt für alle in der Datei deklarierten Typen, ohne dass sie explizit umbrochen werden müssen. Dies ist Teil der Bemühungen von C# 10.0, unproduktives Durcheinander in unseren Quelldateien zu reduzieren.

Beispiel 1-6. Pre-C# 10.0 Namensraum-Deklaration
namespace Averages
{
    public static class AverageCalculator
    {
        ...as before...
    }
}

Der Namensraum gibt normalerweise einen Hinweis auf den Zweck eines Typs. So finden sich z. B. alle Laufzeitbibliothekstypen, die sich auf den Umgang mit Dateien beziehen, im Namensraum Sys⁠tem.​IO, während die Typen, die sich mit Netzwerken beschäftigen, unter System.Net zu finden sind. Namensräume können eine Hierarchie bilden. So enthält der Namensraum des Frameworks System Typen und auch andere Namensräume, wie System.Net, und diese enthalten oft noch mehr Namensräume, wie System.Net.Sockets und System.Net.Mail. Diese Beispiele zeigen, dass Namensräume als eine Art Beschreibung dienen, die dir bei der Navigation in der Bibliothek helfen kann. Wenn du z. B. nach der Behandlung regulärer Ausdrücke suchst, könntest du die verfügbaren Namensräume durchsehen und den System.Text Namensraum entdecken. Dort findest du den Namensraum System.Text.RegularExpressions und kannst dir sicher sein, dass du an der richtigen Stelle gesucht hast.

Namensräume bieten auch eine Möglichkeit, die Eindeutigkeit zu gewährleisten. Der Namensraum, in dem ein Typ definiert ist, ist Teil des vollständigen Namens dieses Typs. So können Bibliotheken kurze, einfache Namen für Dinge verwenden. Die API für reguläre Ausdrücke enthält zum Beispiel eine Klasse Capture, die die Ergebnisse einer Erfassung mit regulären Ausdrücken darstellt. Wenn du an einer Software arbeitest, die sich mit Bildern beschäftigt, wird der Begriff " Capture" häufig für die Erfassung von Bilddaten verwendet, und du denkst vielleicht, dass Capture der beschreibendste Name für eine Klasse in deinem eigenen Code ist. Es wäre ärgerlich, einen anderen Namen wählen zu müssen, nur weil der beste bereits vergeben ist, vor allem, wenn dein Code für die Bilderfassung keine Verwendung für reguläre Ausdrücke hat, du also gar nicht vorhattest, den vorhandenen Typ Capture zu verwenden.

Aber eigentlich ist das in Ordnung. Beide Typen können Capture genannt werden und sie haben trotzdem unterschiedliche Namen. Der vollständige Name des regulären Ausdrucks der Klasse Capture lautet effektivSystem.Text.RegularExpressions.Captureund auch der vollständige Name deiner Klasse würde den enthaltenen Namensraum enthalten (z. B. Spi⁠ffi⁠ngS⁠oft⁠wor⁠ks.​Ima⁠gin⁠g.Ca⁠ptu⁠re).

Wenn du es wirklich willst, kannst du den vollqualifizierten Namen eines Typs jedes Mal schreiben, wenn du ihn verwendest, aber die meisten Entwickler wollen so etwas Mühsames nicht tun. Hier kommen die using Direktiven ins Spiel, die du am Anfang der Beispiele 1-2 und 1-3 sehen kannst. Am Anfang jeder Quelldatei befindet sich in der Regel eine Liste mit Direktiven, die die Namensräume der Typen angeben, die in der Datei verwendet werden sollen. Normalerweise wirst du diese Liste so bearbeiten, dass sie den Anforderungen deiner Datei entspricht. In diesem Beispiel hat das Kommandozeilentool dotnet using Microsoft.VisualStudio.TestTools.UnitTesting; hinzugefügt, als es das Testprojekt erstellt hat. In verschiedenen Kontexten wirst du unterschiedliche Sätze sehen. Wenn du z. B. eine Klasse hinzufügst, die ein UI-Element darstellt, würde Visual Studio verschiedene UI-bezogene Namensräume in die Liste aufnehmen.

Projekte, die auf C# 10.0 oder höher abzielen, haben in der Regel weniger using Direktiven als Projekte, die für ältere Versionen geschrieben wurden (zum Zeitpunkt der Erstellung dieses Artikels sind das fast alle), weil es eine neue Sprachfunktion gibt: globale Verwendungsdirektiven. Wenn wir das Schlüsselwort global vor die Direktive setzen, wie in Beispiel 1-7, gilt die Direktive für alle Dateien in einem Projekt. Das .NET SDK geht noch einen Schritt weiter, indem es in deinem Projekt eine versteckte Datei mit einer Reihe dieser global using Direktiven erzeugt, um sicherzustellen, dass häufig verwendete Namensräume wie System undSystem.Collections.Generic verfügbar sind. (Die genaue Anzahl der Namensräume, die als implizite globale Importe hinzugefügt werden, variiert je nach Projekttyp - bei Webprojekten gibt es zum Beispiel ein paar zusätzliche. Wenn du dich fragst, warum Unit-Test-Projekte nicht bereits das tun, was Beispiel 1-7 tut, liegt das daran, dass das .NET SDK keinen speziellen Projekttyp für Testprojekte hat - es betrachtet sie einfach als eine Art Klassenbibliothek).

Beispiel 1-7. Eine global using Richtlinie
global using Microsoft.VisualStudio.TestTools.UnitTesting;

Mit using Deklarationen wie diesen (entweder pro Datei oder global) kannst du einfach den kurzen, unqualifizierten Namen für eine Klasse verwenden. Die Codezeile, mit der Beispiel 1-1 seine Aufgabe erfüllt, verwendet die Klasse System.Console, aber da das SDK eine implizite global using Direktive für den System Namensraum hinzufügt, kann es sich auf sie als Console beziehen.

Hinweis

Zuvor habe ich das dotnet CLI verwendet, um eine Referenz aus unseremAverages.Tests Projekt zu unserem Averages Projekt hinzuzufügen. Du denkst vielleicht, dass Referenzen überflüssig sind - kann der Compiler nicht anhand der Namensräume herausfinden, welche externen Bibliotheken wir verwenden? Das könnte er, wenn es eine direkte Verbindung zwischen Namensräumen und Bibliotheken oder Paketen gäbe, aber das ist nicht der Fall. Manchmal gibt es eine offensichtliche Verbindung - das beliebte Newtonsoft.Json NuGet-Paket enthält z. B. eine Newtonsoft.Json.dll-Datei, die Klassen im Newtonsoft.Json Namensraum enthält. Aber oft gibt es keine solche Verbindung - die .NET-Laufzeitbibliotheken enthalten eine System.Private.CoreLib.dll-Datei, aber es gibt keinen System.Private.CoreLib Namensraum. Es ist also notwendig, dem Compiler mitzuteilen, von welchen Bibliotheken dein Projekt abhängt und welche Namensräume es verwendet. In Kapitel 12 werden wir uns die Art und Struktur von Bibliotheksdateien genauer ansehen.

Auch bei Namensräumen kann es zu Mehrdeutigkeiten kommen. In einer Quelldatei können zwei Namensräume verwendet werden, die beide eine Klasse mit demselben Namen definieren. Wenn du diese Klasse verwenden willst, musst du sie explizit mit ihrem vollen Namen ansprechen. Wenn du solche Klassen in der Datei häufig verwenden musst, kannst du dir trotzdem etwas Tipparbeit sparen: Du musst den vollen Namen nur einmal verwenden, weil du einen Alias definieren kannst. In Beispiel 1-8 werden Aliase verwendet, um einen Konflikt zu lösen, der mir schon ein paar Mal begegnet ist: Das .NET Desktop UI Framework, die Windows Presentation Foundation (WPF), definiert eine Path Klasse für die Arbeit mit Bézier-Kurven, Polygonen und anderen Formen, aber es gibt auch eine Path Klasse für die Arbeit mit Dateisystempfaden, und du möchtest vielleicht beide Typen zusammen verwenden, um eine grafische Darstellung des Inhalts einer Datei zu erzeugen. Durch das Hinzufügen von using Direktiven für beide Namensräume würde der einfache Name Path mehrdeutig werden, wenn er nicht qualifiziert ist. Aber wie Beispiel 1-8 zeigt, kannst du für beide Namensräume eindeutige Aliase definieren.

Beispiel 1-8. Auflösen von Mehrdeutigkeit mit Aliasen
using System.IO;
using System.Windows.Shapes;
using IoPath = System.IO.Path;
using WpfPath = System.Windows.Shapes.Path;

Mit diesen Aliasen kannst du IoPath als Synonym für die dateibezogene Klasse Path und WpfPath für die grafische Klasse verwenden.

Übrigens kannst du auf Typen in deinem eigenen Namensraum ohne Qualifikation verweisen, ohne dass du eine using Direktive brauchst. Deshalb hat der Testcode in Beispiel 1-3 auch keine using Averages; Direktive. Du fragst dich vielleicht, wie das funktioniert, da der Testcode einen anderen Namensraum deklariert, nämlich Averages.Tests. Um das zu verstehen, müssen wir uns die Verschachtelung von Namensräumen ansehen.

Verschachtelte Namensräume

Wie du bereits gesehen hast, verschachteln die .NET-Laufzeitbibliotheken ihre Namensräume, manchmal recht umfangreich, und du wirst das oft auch tun wollen. Es gibt zwei Möglichkeiten, wie du das tun kannst. Du kannst Namensraum-Deklarationen verschachteln, wie Beispiel 1-9 zeigt.

Beispiel 1-9. Verschachtelung von Namensraum-Deklarationen
namespace MyApp
{
    namespace Storage
    {
        ...
    }
}

Alternativ kannst du auch nur den vollständigen Namensraum in einer einzigen Deklaration angeben, wie Beispiel 1-10 zeigt. Dies ist die am häufigsten verwendete Methode. Diese Einzeldeklaration funktioniert entweder mit der neuen C# 10.0-Deklaration, wie in Beispiel 1-10 gezeigt, oder mit der älteren Deklaration mit geschweiften Klammern.

Beispiel 1-10. Verschachtelter Namensraum mit einer einzigen Deklaration
namespace MyApp.Storage;

Jeder Code, den du in einem verschachtelten Namensraum schreibst, kann nicht nur Typen aus diesem Namensraum verwenden, sondern auch aus den darin enthaltenen Namensräumen, ohne dass eine Qualifikation erforderlich ist. Der Code in den Beispielen 1-9 und 1-10 benötigt keine explizite Qualifikation oder using Direktiven, um Typen aus dem MyApp.Storage oder MyApp Namensraum zu verwenden. Aus diesem Grund musste ich in Beispiel 1-3 keine using Averages; Direktive hinzufügen, um auf AverageCalculator im Averages Namensraum zugreifen zu können: Der Test wurde im Averages.Tests Namensraum deklariert, und da dieser im Averages Namensraum verschachtelt ist, hat der Code automatisch Zugriff auf diesen äußeren Namensraum.

Wenn du verschachtelte Namensräume definierst, wird üblicherweise eine entsprechende Verzeichnishierarchie erstellt. Einige Tools erwarten dies. Obwohl VS Code hier keine besonderen Erwartungen hat, folgt Visual Studio dieser Konvention. Wenn dein Projekt MyApp heißt, fügt es neue Klassen in den MyApp Namensraum ein, wenn du sie dem Projekt hinzufügst. Wenn du jedoch ein neues Verzeichnis im Projekt erstellst, das z. B. Speicherung heißt, legt Visual Studio alle neuen Klassen, die du in diesem Verzeichnis erstellst, im Namensraum MyApp.Storage ab. Auch hier musst du dich nicht daran halten - Visual Studio fügt beim Erstellen der Datei lediglich eine Namespace-Deklaration hinzu, die du beliebig ändern kannst. Der Compiler braucht den Namensraum nicht, um mit deiner Verzeichnishierarchie übereinzustimmen. Da diese Konvention aber von verschiedenen Tools, darunter auch Visual Studio, unterstützt wird, ist es einfacher, wenn du sie befolgst.

Klassen

Nach der Namespace-Deklaration definiert unsere Datei AverageCalculator.cs eine Klasse. Beispiel 1-11 zeigt diesen Teil der Datei. Er beginnt mit dem Schlüsselwort public, das den Zugriff auf diese Klasse durch andere Komponenten ermöglicht. Als Nächstes folgt das Schlüsselwort static, das darauf hinweist, dass diese Klasse nicht instanziiert werden soll - sie bietet nur Operationen auf Klassenebene und keine Funktionen pro Instanz. Dann folgt das Schlüsselwort class, gefolgt vom Namen, und natürlich ist der vollständige Name des Typs aufgrund der Namespace-Deklaration tatsächlich Averages.AverageCalculator. Wie du siehst, verwendet C# geschweifte Klammern ({}), um alles Mögliche abzugrenzen - das haben wir schon bei der älteren (aber immer noch weit verbreiteten) Syntax für die Namespace-Deklaration gesehen, und hier kannst du das Gleiche bei der Klasse und der darin enthaltenen Methode sehen.

Beispiel 1-11. Eine Klasse mit einer Methode
public static class AverageCalculator
{
    public static double ArithmeticMean(string[] args)
    {
        return 1.0;
    }
}

Klassen sind C#s Mechanismus zur Definition von Entitäten, die Zustand und Verhalten kombinieren, ein gängiges objektorientiertes Idiom. Aber diese Klasse enthält nicht mehr als eine einzige Methode. C# unterstützt keine globalen Methoden - jeder Code muss als Mitglied eines Typs geschrieben werden. Diese Klasse ist also nicht besonders interessant - ihre einzige Aufgabe ist es, als Container für die Methode zu fungieren, die die eigentliche Arbeit verrichtet. In Kapitel 3 werden wir weitere interessante Einsatzmöglichkeiten für Klassen kennenlernen.

Wie bei der Klasse habe ich die Methode als public gekennzeichnet, um den Zugriff aus anderen Komponenten zu ermöglichen. Außerdem habe ich sie als statische Methode deklariert, was bedeutet, dass es nicht notwendig ist, eine Instanz des enthaltenen Typs (in diesem FallAverageCalculator) zu erstellen, um die Methode aufzurufen. Das folgende Schlüsselwort double zeigt an, dass der Datentyp, den diese Methode zurückgibt, eine doppelpräzise Gleitkommazahl ist.

Auf die Methodendeklaration folgt der Methodenrumpf, der in diesem Beispiel Code enthält, der einen Platzhalterwert zurückgibt, so dass nur noch der Code innerhalb der geschweiften Klammern, die den Methodenrumpf begrenzen, geändert werden muss. Beispiel 1-12 zeigt einen Code, der den Durchschnitt berechnet, anstatt nur 1,0 zurückzugeben.

Beispiel 1-12. Berechnen des Durchschnitts
return args.Select(numText => double.Parse(numText)).Average();

Dies beruht auf Bibliotheksfunktionen für die Arbeit mit Sammlungen, die zu den Funktionen gehören, die unter dem Namen LINQ bekannt sind und Gegenstand von Kapitel 10 sind. Um kurz zu beschreiben, was hier passiert: Mit der Methode Select können wir eine Operation auf jedes einzelne Element in einer Sammlung anwenden. In diesem Fall ist die Operation, die ich anwende, die Methode double.Parse, eine Funktion der .NET-Laufzeitbibliothek, die einen Textstring, der eine Zahl enthält, in den nativen doppelpräzisen Gleitkommatyp umwandelt. Anschließend leiten wir die umgewandelten Ergebnisse an die Methode Average weiter, die die Berechnung für uns übernimmt.

Wenn ich nun dotnet test erneut ausführe, meldet es, dass alle Tests bestanden wurden. Der Code scheint also zu funktionieren. Ich sehe jedoch ein Problem, wenn ich versuche, dies informell zu überprüfen, indem ich das Programm ausführe, was ich mit diesem Befehl tun kann:

./Averages/bin/Debug/net6.0/Averages 1 2 3 4 5

Damit wird einfach Hello, World! auf dem Bildschirm ausgegeben. Ich habe den Code, der die erforderliche Berechnung durchführt, geschrieben und getestet, aber ich habe ihn noch nicht mit dem Einstiegspunkt des Programms verbunden. Der Code, der beim Start des Programms ausgeführt wird, befindet sich in Program.cs, obwohl dieser Dateiname nichts Besonderes ist. Der Einstiegspunkt des Programms kann in jeder beliebigen Datei stehen. In älteren Versionen von C# wurde der Einstiegspunkt durch die Definition einer static Methode namens Main angegeben, wie in Beispiel 1-2 gezeigt. Ab C# 10.0 kannst du stattdessen eine Datei hinzufügen, die ausführbare Anweisungen enthält, ohne sie explizit in einer Methode in einem Typ zu platzieren, und der C#-Compiler wird dies als Einstiegspunkt behandeln. (Du darfst nur eine Datei in deinem Projekt haben, die auf diese Weise geschrieben wurde, weil dein Programm nur einen Einstiegspunkt haben kann). Wenn ich den gesamten Inhalt von Program.cs durch den in Beispiel 1-13 gezeigten Code ersetze, hat das den gewünschten Effekt.

Beispiel 1-13. Programmeinstiegspunkt mit Argumenten
using Averages;

Console.WriteLine(AverageCalculator.ArithmeticMean(args));

Beachte, dass ich eine using Direktive hinzufügen musste - wenn du die neue, reduzierte Syntax für den Programmeinstiegspunkt von C# 10.0 verwendest, befindet sich der Code in dieser Datei standardmäßig in keinem Namespace, also muss ich angeben, dass ich die Klasse verwenden möchte, die ich im Averages Namespace definiert habe. Danach ruft dieser Code die Methode auf, die ich zuvor geschrieben habe, wobei er args als Argument übergibt, und ruft dann Console.WriteLine auf, um das Ergebnis anzuzeigen. Wenn du diese Art von Programmeinstiegspunkt verwendest, ist args ein besonderer Name - es ist quasi eine implizit definierte lokale Variable, die den Zugriff auf die Befehlszeilenargumente ermöglicht. Dabei handelt es sich um ein Array von Strings, mit einem Eintrag für jedes Argument. Wenn du das Programm noch einmal mit denselben Argumenten wie zuvor ausführen möchtest, musst du zuerst den Befehl dotnet build ausführen, um es neu zu erstellen.

Tipp

Einige Sprachen der C-Familie geben den Dateinamen des Programms selbst als erstes Argument an, weil er Teil dessen ist, was der Benutzer an der Eingabeaufforderung eingegeben hat. C# folgt dieser Konvention nicht. Wenn das Programm ohne Argumente gestartet wird, ist die Länge des Arrays 0. Du hast vielleicht bemerkt, dass der Code damit nicht gut zurechtkommt. Du kannst gerne ein neues Testszenario hinzufügen, das das entsprechende Verhalten definiert, und das Programm entsprechend anpassen.

Einheitstests

Jetzt, wo das Programm funktioniert, möchte ich auf die Tests zurückkommen, denn sie veranschaulichen einige C#-Funktionen, die das Hauptprogramm nicht hat. Wenn du zu Beispiel 1-3 zurückgehst, beginnt es ganz normal: Wir haben eine using Direktive und dann eine Namespace-Deklaration, diesmal für Averages.Tests, passend zum Namen des Testprojekts. Aber die Klasse sieht anders aus. Beispiel 1-14 zeigt den relevanten Teil von Beispiel 1-3.

Beispiel 1-14. Testklasse mit Attribut
[TestClass]
public class WhenCalculatingAverages
{

Unmittelbar vor der Klassendeklaration steht der Text [TestClass]. Dies ist ein Attribut. Attribute sind Anmerkungen, die du auf Klassen, Methoden und andere Merkmale des Codes anwenden kannst. Die meisten von ihnen haben keine eigene Funktion - der Compiler zeichnet die Tatsache, dass das Attribut vorhanden ist, in der kompilierten Ausgabe auf, aber das ist auch schon alles. Attribute sind nur dann nützlich, wenn etwas nach ihnen sucht, deshalb werden sie meist von Frameworks verwendet. In diesem Fall verwende ich das Unit-Testing-Framework von Microsoft, das nach Klassen sucht, die mit dem Attribut TestClass gekennzeichnet sind. Klassen, die nicht mit diesem Attribut versehen sind, werden ignoriert. Attribute sind in der Regel spezifisch für ein bestimmtes Framework, aber du kannst auch deine eigenen definieren, wie wir in Kapitel 14 sehen werden.

Die beiden Methoden der Klasse sind ebenfalls mit Attributen versehen. Beispiel 1-15 zeigt die entsprechenden Auszüge aus Beispiel 1-3. Der Test Runner führt alle Methoden aus, die mit dem Attribut [TestMethod] gekennzeichnet sind.

Beispiel 1-15. Kommentierte Methoden
[TestMethod]
public void SingleInputShouldProduceSameValueAsResult()
...

[TestMethod]
public void MultipleInputsShouldProduceAverageAsResult()
...

Und damit haben wir jedes Element eines Programms und des Testprojekts untersucht, das überprüft, ob es wie vorgesehen funktioniert.

Zusammenfassung

Du hast jetzt die grundlegende Struktur von C#-Programmen gesehen. Ich habe eine Lösung mit zwei Projekten erstellt, eines für die Tests und eines für das Programm selbst. Da es sich um ein einfaches Beispiel handelte, enthielt jedes Projekt nur ein oder zwei Quelldateien von Interesse. Wo nötig, begannen diese Dateien mit using Direktiven, die angeben, welche Typen die Datei verwendet. Für den Einstiegspunkt des Programms wurde der neue, reduzierte Stil von C# 10.0 verwendet, während die beiden anderen Dateien eine konventionellere Struktur hatten: eine Namespace-Deklaration, die den Namespace angibt, den die Datei bevölkert, und eine Klasse, die eine oder mehrere Methoden oder andere Mitglieder, z. B. Felder, enthält.

Wir werden uns die Typen und ihre Mitglieder in Kapitel 3 genauer ansehen, aber zuerst wird sich Kapitel 2 mit dem Code innerhalb der Methoden befassen, in dem wir ausdrücken, was wir mit unseren Programmen machen wollen.

1 Das alte .NET Framework wird noch viele Jahre lang unterstützt werden, aber Microsoft hat erklärt, dass es keine neuen Funktionen erhalten wird.

2.NET Native und NativeAOT tun dies nicht: Sie wurden speziell entwickelt, um ein Laufzeit-JIT zu vermeiden, und bieten daher keine abgestufte Kompilierung.

3 Falls du dich fragst, wie diese Versionsnummern und -daten mit den jährlich wechselnden Veröffentlichungen zusammenpassen, sei dir gesagt, dass der aktuelle Zeitplan mit .NET Core 3.1 eingeführt wurde und es kein .NET Core 4 gab. Als .NET Core in einfaches .NET umbenannt wurde, wurde von 3.1 auf 5.0 umgestellt, um zu betonen, dass es sich um eine Weiterentwicklung von .NET Framework handelt, dessen letzte Version 4.8 ist.

4 oder .NET Core. Die Namensänderungen können hier für Verwirrung sorgen. Eine Komponente, die .NET Core 3.1 unterstützt, funktioniert auch unter .NET 5.0 und .NET 6.0, weil es sich dabei um spätere Versionen derselben Laufzeitumgebung handelt; bei der Auslieferung von .NET 5.0 wurde lediglich das Wort Core weggelassen und die Versionsnummer übersprungen.

Get Programmierung C# 10 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.