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 fast zwei Jahrzehnten im Mittelpunkt des Interesses von Windows-Entwicklern, aber in den letzten Jahren hat es auch auf anderen Plattformen Einzug gehalten. Im Juni 2016 veröffentlichte Microsoft die Version 1.0 von .NET Core, eine plattformübergreifende Version von .NET, mit der in C# geschriebene Webanwendungen, Microservices und Konsolenanwendungen nicht nur unter Windows, sondern auch unter macOS und Linux ausgeführt werden können.

Dieser Vorstoß auf andere Plattformen ging Hand in Hand mit Microsofts Bekenntnis zur Open-Source-Entwicklung. In der Anfangszeit von C# hat Microsoft seinen gesamten Quellcode streng gehütet,1 Aber heute wird so ziemlich alles, was mit C# zu tun hat, offen entwickelt, und Codebeiträge von außerhalb Microsofts sind willkommen. Vorschläge für neue Sprachfeatures werden auf GitHub veröffentlicht, so dass die Community schon in einem frühen Stadium einbezogen werden kann. Im Jahr 2014 wurde die .NET Foundation(https://dotnetfoundation.org/) 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 stehen nun unter der Leitung der Foundation (zusätzlich zu vielen Nicht-Microsoft-Projekten). Dazu gehören Microsofts C#-Compiler, der unter https://github.com/dotnet/roslyn zu finden ist , und auch .NET Core , das unter https://github.com/dotnet/core zu finden ist und die Laufzeitumgebung, die Klassenbibliothek und die Werkzeuge zur Erstellung von .NET-Projekten umfasst.

Warum C#?

Obwohl es viele Möglichkeiten gibt, C# zu verwenden, sind andere Sprachen immer eine Option. Warum solltest du C# ihnen vorziehen? 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. Sie bietet Unterstützung für asynchrone Programmierung.

In letzter Zeit hat C# an Flexibilität bei der Speicherverwaltung gewonnen. 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. Deshalb wurden in C# 7.2 (veröffentlicht 2017) verschiedene Funktionen hinzugefügt, die eine explizitere Speicherverwaltung ermöglichen, so dass du die Möglichkeit hast, die einfache Entwicklung gegen die Laufzeitleistung einzutauschen, ohne dabei die Typsicherheit zu verlieren. Dadurch kann C# in bestimmten leistungsrelevanten Anwendungen eingesetzt werden, die jahrelang weniger sicheren Sprachen wie C und C++ vorbehalten waren.

Natürlich existieren Sprachen nicht im luftleeren Raum - hochwertige Bibliotheken mit einer breiten Palette von Funktionen sind unerlässlich. Einige elegante und akademisch schöne Sprachen sind so lange glorreich, bis du etwas Prosaisches tun willst, 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 vollständigen und bequemen Zugang zu den Diensten der zugrunde liegenden Plattform bieten. C# ist hier dank seiner Laufzeitumgebung, der Klassenbibliothek und der umfangreichen Unterstützung von Drittanbieterbibliotheken sehr gut aufgestellt.

.NET umfasst sowohl die Laufzeit als auch die Hauptklassenbibliothek, 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 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.

Zusätzlich zur Laufzeitumgebung gibt es eine umfangreiche Klassenbibliothek. Diese Bibliothek bietet Wrapper für viele Funktionen des zugrundeliegenden Betriebssystems (OS), stellt aber auch eine beträchtliche Menge an eigenen Funktionen zur Verfügung, wie z. B. Sammelklassen oder JSON-Verarbeitung.

Die in .NET integrierte Klassenbibliothek ist nicht alles - viele andere Systeme bieten ihre eigenen .NET-Bibliotheken. So gibt es zum Beispiel umfangreiche Bibliotheken, mit denen C#-Programme beliebte Cloud-Dienste nutzen können. Wie nicht anders zu erwarten, bietet Microsoft umfassende .NET-Bibliotheken für die Arbeit mit den Diensten seiner Cloud-Plattform Azure an. Auch Amazon bietet ein umfassendes SDK für die Nutzung der Amazon Web Services (AWS) in C# und anderen .NET-Sprachen. Und Bibliotheken müssen nicht zwangsläufig mit Frameworks verbunden sein. Es gibt ein großes Ökosystem von .NET-Bibliotheken, einige kommerziell, andere frei und quelloffen, darunter mathematische Hilfsprogramme, Parsing-Bibliotheken und Komponenten für die Benutzeroberfläche (UI), um nur einige zu nennen. Selbst wenn du Pech hast und ein Betriebssystem verwenden musst, für das es keine .NET-Bibliotheken gibt, bietet C# verschiedene Mechanismen für die Arbeit mit anderen Arten von APIs, z. B. die C-ähnlichen APIs in Win32, macOS und Linux oder APIs, die auf dem Component Object Model (COM) in Windows basieren.

Da es .NET schon seit etwa 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- und Plattformfunktionen.

Definierende Merkmale von C#

Auch wenn das offensichtlichste Merkmal von C# seine Syntax aus der C-Familie ist, so ist es doch die erste Sprache, die für die Welt der CLR entwickelt wurde. Wie der Name schon sagt, ist die CLR flexibel genug, um viele Sprachen zu unterstützen, aber es gibt einen wichtigen Unterschied zwischen einer Sprache, die erweitert wurde, um die CLR zu unterstützen, und einer, die sie in den Mittelpunkt ihres Designs stellt. Die .NET-Erweiterungen in Microsofts C++-Compiler sind ein gutes Beispiel dafür: Die Syntax für die Verwendung dieser Funktionen unterscheidet sich deutlich von der des Standard-C++ und macht einen klaren Unterschied zwischen der nativen Welt von C++ und der Außenwelt der CLR. Aber auch ohne unterschiedliche Syntax,2 gäbe es immer noch Reibungen, wenn zwei Welten unterschiedlich funktionieren. Wenn du z. B. eine dynamisch veränderbare Zahlensammlung brauchst, solltest du dann eine Standard-C++-Sammlungsklasse wie vector<int> oder eine aus .NET wie List<int> verwenden? Egal, wofür du dich entscheidest, es wird manchmal der falsche Typ sein: C++-Bibliotheken werden nicht wissen, was sie mit einer .NET-Sammlung anfangen sollen, während .NET-APIs den C++-Typ nicht verwenden können.

C# umfasst .NET, sowohl die Laufzeit als auch die Klassenbibliothek, so dass sich diese Probleme nicht stellen. In dem gerade besprochenen Szenario hat List<int> keinen Konkurrenten. Es gibt keine Reibungen bei der Verwendung der .NET-Klassenbibliothek, weil sie für dieselbe Welt wie C# entwickelt wurde.

Die erste Version von C# bot ein Programmiermodell, das sehr eng mit dem zugrunde liegenden Modell der CLR verwandt war. Im Laufe der Jahre hat C# nach und nach seine eigenen Abstraktionen hinzugefügt, die jedoch so gestaltet wurden, dass sie gut zur CLR passen. 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.

Verwalteter Code und die CLR

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 Managed Model ist, dass die Ausgabe des Compilers nicht an eine einzige CPU-Architektur gebunden ist. Du kannst eine .NET-Komponente schreiben, die auf der 32-Bit-x86-Architektur läuft, die seit Jahrzehnten in PCs verwendet wird, die aber auch auf der neueren 64-Bit-Version (x64) und sogar auf völlig anderen Architekturen wie ARM gut funktioniert. (Mit .NET Core wurde zum Beispiel die Fähigkeit eingeführt, auf ARM-basierten Geräten wie dem Raspberry Pi zu laufen). Mit einer Sprache, die direkt in Maschinencode kompiliert wird, müsstest du für jede dieser Architekturen unterschiedliche Binärdateien erstellen. Mit .NET kannst du jedoch eine einzige Komponente kompilieren, die auf allen Plattformen läuft und sogar auf Plattformen, die zum Zeitpunkt der Kompilierung nicht unterstützt wurden, wenn in Zukunft eine geeignete Laufzeitumgebung verfügbar ist. Generell gilt, dass jede Verbesserung der Codegenerierung der CLR - sei es die Unterstützung neuer CPU-Architekturen oder nur Leistungsverbesserungen für bestehende - 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. Alle Codes, die auf aktuellen Versionen von .NET Core laufen, profitieren davon, auch Codes, die Jahre vor dieser Verbesserung geschrieben wurden.

Der genaue Zeitpunkt, zu dem die CLR ausführbaren Maschinencode erzeugt, kann variieren. Normalerweise verwendet sie eine Methode namens Just-in-Time (JIT) Kompilierung, bei der jede einzelne Funktion beim ersten Mal kompiliert wird. Das muss aber nicht so funktionieren. Es gibt verschiedene Möglichkeiten, wie .NET-Code im Voraus kompiliert werden kann (AoT). Es gibt ein Tool namens NGen, das dies als Schritt nach der Installation erledigen kann. Windows Store Apps, die für die Universal Windows Platform (UWP) erstellt werden, verwenden die .NET Native Build-Tools, die dies bereits während des Builds tun. .NET Core 3.0 fügt ein neues Tool namens crossgen hinzu, mit dem jede .NET Core-Anwendung (nicht nur UWP-Apps) die Generierung von nativem Code zur Build-Zeit nutzen kann. Die Generierung von ausführbarem Code kann jedoch auch dann noch zur Laufzeit erfolgen, wenn du diese Tools verwendest3-Laufzeitkompilierung kann eine Methode dynamisch neu kompilieren, um sie besser für die Verwendung zur Laufzeit zu optimieren. (Dies ist unabhängig davon, ob du JIT oder AoT verwendest.) Die virtualisierte Ausführung von Managed Execution ist so konzipiert, dass solche Dinge für deinen Code unsichtbar sind, obwohl sie sich gelegentlich nicht nur durch die Leistung bemerkbar machen. Die virtualisierte Ausführung lässt zum Beispiel einen gewissen Spielraum, wann und wie die Laufzeitumgebung bestimmte Initialisierungsarbeiten durchführt, und manchmal kannst du sehen, wie die Ergebnisse der Optimierungen dazu führen, dass Dinge in einer überraschenden Reihenfolge passieren.

Verwalteter Code verfügt über allgegenwärtige Typinformationen. Die Dateiformate, die von der CLI vorgegeben werden, erfordern diese Informationen, weil sie bestimmte Laufzeitfunktionen ermöglichen. NET bietet zum Beispiel verschiedene automatische Serialisierungsdienste an, mit denen Objekte in binäre oder textuelle Darstellungen ihres Zustands umgewandelt werden können, die dann 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.

Ziehe Allgemeinheit der Spezialisierung vor

C# bevorzugt allgemeine Sprachfunktionen gegenüber speziellen. Im Laufe der Jahre hat Microsoft C# mehrmals erweitert, und die Designer der Sprache haben immer bestimmte Szenarien für neue Funktionen im Sinn. Sie haben sich jedoch stets bemüht, sicherzustellen, dass jedes neue Element, das sie hinzufügen, über diese primären Szenarien hinaus nützlich ist.

Vor einigen Jahren beschloss Microsoft beispielsweise, 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, aber Microsoft hat dies erreicht, ohne der Sprache eine direkte Unterstützung für den Datenzugriff hinzuzufügen. Stattdessen führte Microsoft eine Reihe ganz unterschiedlicher Funktionen ein. 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 (die mit C# 3.0 eingeführt wurden) machen es zum Beispiel sehr 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 für Datenbanken.

Ein Beispiel für diese Philosophie der Allgemeingültigkeit war 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 von Microsoft, 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 bei der Erstellung 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 Einführung 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 C#-Versionen 7 und 8 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. Es gibt Spezifikationen, die das Sprach- und Laufzeitverhalten für alle C#-Implementierungen festlegen, wie im nächsten Abschnitt, "C#, die CLR und Standards", beschrieben wird. Dies hat es möglich gemacht, dass mehrere Implementierungen von C# und der Laufzeitumgebung entstanden sind. Zum Zeitpunkt der Erstellung dieses Artikels sind drei davon weit verbreitet: .NET Framework, .NET Core und Mono. Etwas verwirrend ist, dass Microsoft hinter allen dreien steht, obwohl das ursprünglich nicht so war.

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 .NET ist ein Markenname von Microsoft). 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 half Mono, seine Nische zu finden, denn heute wird es hauptsächlich für die Entwicklung plattformübergreifender Anwendungen für mobile Geräte in C# verwendet. Mono war von Anfang an Open Source und wurde im Laufe seines Bestehens von einer Vielzahl von Unternehmen unterstützt. Zum Zeitpunkt, an dem ich diesen Artikel schreibe, wird Mono seit 2011 von einem Unternehmen namens Xamarin verwaltet. Microsoft hat Xamarin im Jahr 2016 übernommen und behält es vorerst als eigenständige Marke bei und positioniert seine Mono-Laufzeitumgebung als die Möglichkeit, C#-Code auf mobilen Geräten auszuführen.

Und was ist mit den beiden anderen Implementierungen, die beide .NET heißen?

Viele Microsoft .NETs (vorübergehend)

Etwa sieben Jahre lang gab es immer nur eine aktuelle Version von .NET, aber seit 2008 ist das Bild unübersichtlicher geworden. Das lag zunächst daran, dass spezielle Varianten von .NET in Verbindung mit verschiedenen UI-Plattformen kamen und gingen, darunter Silverlight, verschiedene Windows Phone-Varianten und die Einführung von Store Applications in Windows 8. Obwohl einige davon noch unterstützt werden, sind sie alle Sackgassen mit Ausnahme der Store Applications, die zu Universal Windows Platform (UWP) Apps wurden. UWP ist zu .NET Core übergegangen, so dass diese anderen .NET-Varianten überflüssig geworden sind.

Aber selbst wenn man diese nicht mehr existierenden Forks von .NET ignoriert, bietet Microsoft immer noch zwei aktuelle Versionen von .NET an: das .NET Framework (nur für Windows, Closed-Source) und .NET Core (plattformübergreifend, Open Source). Im Mai 2019 hat Microsoft angekündigt, dass es im November 2020 zu einer einzigen aktuellen Version zurückkehren will. Langfristig wird dies die Verwirrung verringern, aber kurzfristig verkompliziert es die Dinge noch mehr, da eine weitere Version eingeführt wird, die man kennen muss.

Ein etwas verwirrender Aspekt dabei sind die geringfügigen Unterschiede in der Namensgebung der verschiedenen .NETs. In den ersten 15 Jahren oder so bedeutete .NET Framework die Kombination aus zwei Dingen: einer Laufzeit und einer Klassenbibliothek. Die Laufzeitumgebung wurde CLR genannt. Die Klassenbibliothek trug verschiedene Namen, darunter Base Class Library (BCL; ein verwirrender Name, denn die ECMA-Spezifikationen definieren den Begriff "BCL" als etwas viel Engeres) oder Framework Class Library.

Heute haben wir auch .NET Core. Seine Laufzeit heißt .NET Core Common Language Runtime (oder einfach CoreCLR), was ein einfacher Name ist: Wir können von der .NET Core CLR oder der .NET Framework CLR sprechen, und es ist offensichtlich, welche wir meinen. Und wenn ich in diesem Buch von der CLR oder der Runtime spreche, ohne sie näher zu beschreiben, dann deshalb, weil ich damit etwas sage, das für beide Implementierungen gilt. Leider nennt .NET Core seine Klassenbibliothek das .NET Core Framework (oder CoreFX). Das ist nicht hilfreich, denn vor .NET Core war das Framework eine Kombination aus der CLR und der Bibliothek. Und um die Sache noch komplizierter zu machen, bezeichnen viele Leute bei Microsoft das .NET Framework als "Desktop"-Framework, um klarzustellen, dass sie nicht von .NET Core sprechen. (Das war schon immer verwirrend, weil viele Leute diese "Desktop"-Version für Serveranwendungen nutzen. Außerdem war die allererste Veröffentlichung von .NET Core für die UWP, die nur Windows-Anwendungen unterstützte. Es dauerte ein Jahr, bis Microsoft eine unterstützte Version herausbrachte, die auch etwas anderes konnte.4 Und jetzt, wo .NET Core 3.0 die Unterstützung für die beiden .NET-Desktop-UI-Frameworks - Windows Presentation Foundation (WPF) und Windows Forms - hinzugefügt hat, werden die meisten neuen Desktop-Anwendungen auf .NET Core abzielen und nicht auf den sogenannten .NET-"Desktop"). Für den Fall, dass das noch nicht ganz klar ist, fasst Tabelle 1-1 die aktuelle Situation zusammen.

Tabelle 1-1. Die Namen der Komponenten von .NET
Plattform Laufzeit Klassenbibliothek

.NET Framework (auch bekannt als .NET Desktop)

.NET CLR

.NET Framework Klassenbibliothek

.NET Core

.NET Core CLR

.NET Core Framework

Wenn Microsoft an seinem Plan festhält, werden die Namen im Jahr 2020 wieder angepasst und sowohl .NET Core als auch .NET Framework werden durch das einfache ".NET" ersetzt. Microsoft hat sich zum Zeitpunkt der Erstellung dieses Artikels noch nicht auf endgültige Namen für die entsprechenden Laufzeit- und Bibliotheksteile geeinigt.

Aber bis dahin haben wir zwei "aktuelle" Versionen. Jede kann Dinge tun, die die andere nicht kann, deshalb werden beide gleichzeitig ausgeliefert. .NET Framework läuft nur unter Windows, während .NET Core Windows, macOS und Linux unterstützt. Dadurch ist das .NET Framework zwar weniger weit verbreitet, aber es kann einige Windows-spezifische Funktionen unterstützen. So gibt es zum Beispiel einen Abschnitt der .NET Framework-Klassenbibliothek, der für die Arbeit mit Windows-Sprachsynthese- und -Erkennungsdiensten vorgesehen ist. Das ist bei .NET Core nicht möglich, weil es unter Linux laufen könnte, wo entsprechende Funktionen entweder nicht existieren oder zu unterschiedlich sind, um über die gleiche .NET-API dargestellt zu werden.

Das .NET, das 2020 auf den Markt kommen soll, ist im Wesentlichen die nächste Version von .NET Core, nur mit einem schmissigeren Namen. .NET Core ist der Ort, an dem die meisten neuen Entwicklungen von .NET in den letzten Jahren stattgefunden haben. .NET Framework wird zwar noch vollständig unterstützt, gerät aber bereits ins Hintertreffen. Die Version 3.0 von Microsofts Web Application Framework, ASP.NET Core, läuft zum Beispiel nur auf .NET Core und nicht auf .NET Framework. Die Ausmusterung von .NET Framework und die Beförderung von .NET Core zum einzig wahren .NET ist also der unausweichliche Abschluss eines Prozesses, der schon seit einigen Jahren im Gange ist.

Mehrere .NET-Versionen mit .NET Standard anvisieren

Die Vielzahl der Laufzeiten mit ihren jeweils unterschiedlichen Versionen der Klassenbibliotheken stellt eine Herausforderung für jeden dar, der seinen Code anderen Entwicklern zur Verfügung stellen möchte. Unter http://nuget.org gibt es ein Paket-Repository für .NET-Komponenten . Dort veröffentlicht Microsoft alle .NET-Bibliotheken, die nicht in .NET selbst integriert sind, und dort veröffentlichen auch die meisten .NET-Entwickler die Bibliotheken, die sie weitergeben möchten. Aber für welche Version solltest du bauen? Das ist eine zweidimensionale Frage: Es gibt die spezifische Implementierung (.NET Core, .NET Framework, Mono) und auch die Version (z. B. .NET Core 2.2 oder 3.0, .NET Framework 4.7.2 oder 4.8). Und dann gibt es noch die älteren .NET-Varianten wie Windows Phone oder Silverlight - viele davon werden von Microsoft immer noch unterstützt, unter anderem durch verschiedene Bibliotheken auf NuGet. Viele Autoren beliebter Open-Source-Pakete, die über NuGet vertrieben werden, unterstützen auch eine Vielzahl älterer Framework-Typen und -Versionen.

Ursprünglich haben die Menschen mit mehreren Versionen gearbeitet, 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 auf verschiedene Varianten von .NET abzielen. 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 Laufzeiten 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 Core. Selbst wenn der Quellcode der Komponente vollständig mit .NET Core 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 Core bereitgestellt hat, konntest du sie nicht verwenden. Das war für alle schlecht. Die Autoren von Komponenten sahen sich gezwungen, neue Varianten ihrer Komponenten zu entwickeln, und da dies davon abhängt, dass die Autoren die Bereitschaft und die Zeit haben, diese Arbeit zu erledigen, mussten die Nutzer von Komponenten feststellen, dass nicht alle Komponenten, die sie verwenden wollten, auf der von ihnen gewünschten Plattform verfügbar waren.

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

.NET-Bibliotheken, die auf NuGet veröffentlicht werden, zielen auf die niedrigste Version des .NET-Standards ab, die sie unterstützen können, um eine möglichst große Reichweite zu gewährleisten. In den Versionen 1.1 bis 1.6 wurden nach und nach mehr Funktionen hinzugefügt, um eine geringere Anzahl von Zielen zu unterstützen. (Wenn du z.B. eine Komponente des .NET Standard 1.3 auf dem .NET Framework verwenden willst, brauchst du das .NET Framework 4.6 oder höher). .NET Standard 2.0 war ein größerer Sprung nach vorn und markiert einen wichtigen Punkt in der Entwicklung von .NET Standard: Nach den aktuellen Plänen von Microsoft wird dies die höchste Versionsnummer sein, die auf .NET Framework laufen kann. Die Versionen von .NET Framework ab 4.7.2 unterstützen es vollständig, aber .NET Standard 2.1 wird weder jetzt noch in Zukunft auf einer Version von .NET Framework laufen. Er läuft auf .NET Core 3.0 und .NET (d.h. auf zukünftigen Versionen von .NET Core). Zukünftige Versionen der Mono-Laufzeitumgebung von Xamarin werden es wahrscheinlich auch unterstützen, aber das ist das Ende des klassischen .NET Frameworks.

Was bedeutet das alles für C#-Entwickler? Wenn du Code schreibst, der nie außerhalb eines bestimmten Projekts verwendet wird, wirst du normalerweise nur die neueste Version von .NET Core verwenden. Wenn du eine Windows-spezifische Funktion brauchst, die diese nicht bietet, kannst du auch .NET Framework verwenden, und du kannst jedes NuGet-Paket verwenden, das auf .NET Standard abzielt, bis einschließlich v2.0 (was bedeutet, dass die überwältigende Mehrheit der NuGet-Pakete für dich verfügbar ist). Wenn du Bibliotheken schreibst, die du weitergeben willst, solltest du stattdessen .NET Standard verwenden. Microsofts Entwicklungstools wählen standardmäßig .NET Standard 2.0 für neue Klassenbibliotheken, was eine vernünftige Wahl ist - du könntest deine Bibliothek für ein breiteres Publikum öffnen, indem du zu einer niedrigeren Version übergehst, aber heutzutage sind die Versionen von .NET, die .NET Standard 2.0 unterstützen, weit verbreitet, so dass du ältere Versionen nur in Betracht ziehen solltest, wenn du Entwickler unterstützen willst, die noch ältere .NET Frameworks verwenden. (Microsoft tut dies in den meisten seiner NuGet-Bibliotheken, aber du musst dich nicht unbedingt an das gleiche Regime der Unterstützung für ältere Versionen binden). Wenn du bestimmte neuere Funktionen nutzen willst (z. B. die in Kapitel 18 beschriebenen speichereffizienten Typen), musst du möglicherweise eine neuere Version von .NET Standard verwenden. In jedem Fall stellen die Entwicklungswerkzeuge sicher, dass du nur die APIs verwendest, die in der Version von .NET Standard verfügbar sind, für die du die Unterstützung erklärst.

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 und Visual Studio Code

Microsoft bietet drei Desktop-Entwicklungsumgebungen an: Visual Studio, Visual Studio for Mac und Visual Studio Code. 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. Es ist am längsten auf dem Markt - genauso lange wie C# - und stammt somit aus der Zeit vor der Open-Source-Entwicklung, ohne dass es auf die Open-Source-Entwicklung umgestellt wurde. Die verschiedenen verfügbaren Editionen reichen von kostenlos bis sehr teuer.

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 Microsofts Cloud-Plattform Azure entwickelt und dort bereitgestellt wurden. Die Funktion Live Share bietet eine bequeme Möglichkeit für die Zusammenarbeit von Entwicklern an entfernten Standorten, um das Pairing oder die Codeüberprüfung zu unterstützen. Von den drei hier beschriebenen Umgebungen verfügt sie über die umfangreichsten Refactoring-Funktionen.

2017 hat Microsoft Visual Studio für Mac veröffentlicht. Dabei handelt es sich nicht um eine Portierung der Windows-Version. Es ist aus einem Produkt 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. Xamarin war ursprünglich ein unabhängiges Produkt, aber als Microsoft, wie bereits erwähnt, das Unternehmen aufkaufte, das es entwickelte, integrierte Microsoft verschiedene Funktionen aus der Windows-Version von Visual Studio, als es das Produkt unter die Marke Visual Studio stellte.

Visual Studio Code (oft als 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 es sich auf allen Betriebssystemen um dasselbe Programm handelt.) 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 installierst, 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 es in seiner ursprünglichen Form weniger eine integrierte Entwicklungsumgebung (IDE) als vielmehr ein einfacher Texteditor ist, ist es dank seines Erweiterungsmodells ziemlich leistungsfähig. Die große Auswahl an Erweiterungen hat dazu geführt, dass VS Code auch außerhalb der Microsoft-Sprachwelt bemerkenswert populär geworden ist, was wiederum einen positiven Kreislauf in Gang gesetzt hat, in dem die Zahl der Erweiterungen noch größer geworden ist.

Visual Studio bietet den einfachsten Weg, um in C# einzusteigen - du musst keine Erweiterungen installieren oder die Konfiguration ändern, um loszulegen. Deshalb beginne ich mit einer kurzen Einführung in die Arbeit mit Visual Studio.

Tipp

Du kannst die kostenlose Version von Visual Studio, genannt Visual Studio Community, von https://www.visualstudio.com/ herunterladen .

Jedes nicht-triviale C#-Projekt hat mehrere Quellcodedateien, die in Visual Studio zu einem Projekt gehören. Jedes Projekt erzeugt eine einzige Ausgabe, das sogenannte Build-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 in der Regel die Dateierweiterung .exe, während Bibliotheken die Endung .dll (historisch gesehen die Abkürzung für Dynamic Link Library) haben. In .NET Core wird jedoch der gesamte erzeugte Code in .dll-Dateien gespeichert. Ab .NET Core 3.0 kann es eine ausführbare Bootstrapping-Datei (mit der Dateierweiterung .exe unter Windows) erzeugen, aber diese startet nur die Laufzeit und lädt dann die .dll, die die wichtigste kompilierte Ausgabe enthält. .NET Framework kompiliert die Anwendung direkt in eine selbst-bootstrapping .exe (ohne separate .dll). In beiden Fällen besteht der einzige Unterschied zwischen der kompilierten Hauptausgabe einer Anwendung und einer Bibliothek darin, dass erstere einen Anwendungseinstiegspunkt 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.

Projektdateien haben normalerweise eine Endung, die auf proj endet. Die meisten C#-Projekte haben zum Beispiel die Erweiterung .csproj, während C++-Projekte die Endung .vcxproj haben. Wenn du diese Dateien mit einem Texteditor untersuchst, wirst du feststellen, dass sie normalerweise XML enthalten. (Das ist nicht immer der Fall. Visual Studio ist erweiterbar, und jede Art von Projekt wird durch ein Projektsystem definiert, das jedes beliebige Format verwenden kann.) Diese Dateien beschreiben den Inhalt des Projekts und legen fest, wie es erstellt werden soll. Das XML-Format, das Visual Studio für C#-Projektdateien verwendet, kann auch vom msbuild-Tool und vom dotnet-Kommandozeilentool verarbeitet werden, wenn du das .NET Core SDK installiert hast, mit dem du Projekte von der Kommandozeile aus erstellen kannst. VS Code kann auch mit diesen Dateien arbeiten.

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 Unit-Tests für jedes der Hauptprojekte enthalten.

Visual Studio hilft dir bei der Arbeit mit mehreren zusammenhängenden Projekten durch eine sogenannte Projektmappe. Eine Projektmappe ist einfach eine Sammlung von Projekten, die zwar in der Regel miteinander verbunden sind, aber nicht zwangsläufig - eine Projektmappe ist eigentlich nur ein Container. Du kannst die aktuell geladene Projektmappe und alle ihre Projekte im Projektmappen-Explorer von Visual Studio sehen. Abbildung 1-1 zeigt eine Projektmappe mit zwei Projekten. (Ich verwende hier Visual Studio 2019, die neueste Version zum Zeitpunkt des Schreibens). Der Solution Explorer zeigt eine Baumansicht, in der du jedes Projekt aufklappen kannst, um die zugehörigen Dateien zu sehen. Normalerweise ist dieses Fenster oben rechts in Visual Studio geöffnet, aber es kann ausgeblendet oder geschlossen werden. Du kannst es mit dem Menüpunkt Ansicht→Solution Explorer wieder öffnen.

Solution Explorer
Abbildung 1-1. Lösungs-Explorer

Visual Studio kann ein Projekt nur laden, wenn es Teil einer Projektmappe ist. Wenn du ein neues Projekt erstellst, kannst du es zu einer bestehenden Projektmappe hinzufügen, aber wenn du das nicht tust, erstellt Visual Studio eine für dich. Wenn du versuchst, eine bestehende Projektdatei zu öffnen, sucht Visual Studio nach einer zugehörigen Projektmappe und wenn es keine findet, erstellt es eine. Das liegt daran, dass viele Vorgänge in Visual Studio auf die aktuell geladene Projektmappe beschränkt sind. Wenn du deinen Code erstellst, ist es normalerweise die Lösung, die du erstellst. Konfigurationseinstellungen, wie z. B. die Wahl zwischen Debug- und Release-Builds, werden auf der Ebene der Projektmappe gesteuert. Mit der globalen Textsuche können alle Dateien in der Projektmappe durchsucht werden.

Eine Lösung ist nur eine weitere Textdatei mit der Erweiterung .sln. Seltsamerweise handelt es sich dabei nicht um eine XML-Datei - Lösungsdateien verwenden ihr eigenes textbasiertes Format, das msbuild und VS Code allerdings verstehen. Wenn du dir den Ordner mit deiner Lösung ansiehst, wirst du vielleicht auch einen .vs-Ordner entdecken. (Visual Studio markiert diesen Ordner als versteckt, aber wenn du den Windows Datei Explorer so konfiguriert hast, dass er versteckte Dateien anzeigt, wie es bei Entwicklern oft der Fall ist, wirst du ihn sehen). Dieser Ordner enthält benutzerspezifische Einstellungen, z. B. welche Dateien du geöffnet hast und welches Projekt oder welche Projekte beim Starten von Debug-Sitzungen gestartet werden sollen. So wird sichergestellt, dass beim Öffnen eines Projekts alles mehr oder weniger dort ist, wo du es bei deiner letzten Arbeit an dem Projekt gelassen hast. Da diese Einstellungen benutzerspezifisch sind, legst du normalerweise keine .vs-Ordner in der Versionskontrolle an.

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 immer 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 und eine neue Lösung erstellst, und gehe dann die verschiedenen Funktionen durch, die Visual Studio einem neuen C#-Projekt als Einführung in die Sprache hinzufügt. Außerdem zeige ich dir, wie du ein Unit-Test-Projekt zur Lösung hinzufügst.

Hinweis

Der nächste Abschnitt ist für Entwickler gedacht, die neu in Visual Studio sind. Dieses Buch richtet sich an erfahrene Entwickler, setzt aber keine Vorkenntnisse in C# oder Visual Studio voraus. Wenn du also bereits mit den grundlegenden Funktionen von Visual Studio vertraut bist, solltest du den nächsten Abschnitt schnell überfliegen.

Anatomie eines einfachen Programms

Wenn du Visual Studio 2019 verwendest, ist der einfachste Weg, ein neues Projekt zu erstellen, das Fenster "Erste Schritte", das sich öffnet, wenn du es startest (siehe Abbildung 1-2).

Visual Studio's Get started window
Abbildung 1-2. Das Fenster Erste Schritte

Wenn du unten rechts auf die Schaltfläche "Neues Projekt erstellen" klickst, wird das Dialogfeld "Neues Projekt" geöffnet. Wenn Visual Studio bereits läuft (oder wenn du eine ältere Version verwendest, die dieses Fenster nicht anzeigt), kannst du alternativ den Menüpunkt Datei→Neu→Projekt in Visual Studio verwenden oder, wenn du Tastaturkürzel bevorzugst, Strg-Umschalt-N eingeben. Jede dieser Aktionen öffnet das Dialogfeld "Neues Projekt erstellen", das in Abbildung 1-3 dargestellt ist.

Visual Studio's Create a new project dialog
Abbildung 1-3. Das Dialogfeld Neues Projekt erstellen

In diesem Fenster wird eine Liste von Anwendungstypen angezeigt. Die genaue Auswahl hängt davon ab, welche Edition von Visual Studio du installiert hast und welche Entwicklungs-Workloads du bei der Installation ausgewählt hast. Wenn du mindestens einen der Workloads installiert hast, der C# enthält, solltest du die Option zum Erstellen einer Konsolenanwendung (.NET Core) sehen. Wenn du diese Option auswählst und auf Weiter klickst, wird das Dialogfeld "Konfigurieren Sie Ihr neues Projekt" angezeigt, das in Abbildung 1-4 dargestellt ist.

Hier kannst du einen Namen für dein neues Projekt und auch für die darin enthaltene Lösung (die standardmäßig denselben Namen trägt) wählen. Du kannst auch den Speicherort für das Projekt auf der Festplatte wählen. Das Feld Projektname beeinflusst drei Dinge. Es bestimmt den Namen der .csproj-Datei auf der Festplatte. Es bestimmt auch den Dateinamen der kompilierten Ausgabe. Schließlich legt es den Standard-Namensraum für neu erstellten Code fest, den ich bei der Vorstellung des Codes erläutern werde. (Du kannst jeden dieser Werte später ändern, wenn du möchtest.)

The Configure your new project dialog
Abbildung 1-4. Das Dialogfeld Dein neues Projekt konfigurieren

Visual Studio bietet ein Kontrollkästchen "Projektmappe und Projekt im selben Verzeichnis ablegen", mit dem du entscheiden kannst, wie die zugehörige Projektmappe erstellt wird. Wenn du diese Option aktivierst, haben Projekt und Projektmappe denselben Namen und befinden sich im selben Ordner auf der Festplatte. Wenn du jedoch vorhast, deiner neuen Lösung mehrere Projekte hinzuzufügen, möchtest du in der Regel, dass sich die Lösung in einem eigenen Ordner befindet, wobei jedes Projekt in einem Unterordner gespeichert wird. Wenn du dieses Kontrollkästchen nicht aktivierst, richtet Visual Studio die Lösung so ein und aktiviert außerdem das Textfeld "Lösungsname", damit du der Lösung bei Bedarf einen anderen Namen als dem ersten Projekt geben kannst. Da ich vorhabe, neben dem Programm auch ein Unit-Test-Projekt zur Projektmappe hinzuzufügen, habe ich das Kontrollkästchen nicht angekreuzt. Ich habe den Projektnamen auf HelloWorld gesetzt und Visual Studio hat den Namen der Projektmappe entsprechend angepasst, womit ich sehr zufrieden bin. Mit einem Klick auf Erstellen wird mein neues C#-Projekt erstellt. Ich habe jetzt also eine Projektmappe mit einem einzigen Projekt darin.

Hinzufügen eines Projekts zu einer bestehenden Lösung

Um der Projektmappe ein Unit-Test-Projekt hinzuzufügen, kann ich im Projektmappen-Explorer mit der rechten Maustaste auf den Projektmappenknoten (ganz oben) klicken und "Hinzufügen" oder "Neues Projekt" wählen. Es öffnet sich ein Dialogfeld, das fast identisch mit dem in Abbildung 1-3 ist, aber stattdessen den Titel "Neues Projekt hinzufügen" trägt. Ich möchte ein Testprojekt hinzufügen. Ich könnte durch die Liste der Projekttypen blättern, aber es gibt schnellere Wege. Ich könnte "Test" in das Suchfeld am oberen Rand des Dialogs eingeben. Oder ich klicke auf die Schaltfläche "Projekttyp" oben rechts und wähle "Test" aus der Dropdown-Liste. In beiden Fällen werden mehrere verschiedene Testprojekttypen angezeigt. Wenn du andere Sprachen als C# siehst, klicke auf die Schaltfläche "Sprache" neben dem Suchfeld, um nur C# zu finden. Auch dann siehst du einige Projekttypen, denn Visual Studio unterstützt verschiedene Test-Frameworks. Ich wähle MSTest Test Project (.NET Core).

Wenn du auf Weiter klickst, öffnet sich wieder der Dialog "Konfiguriere dein neues Projekt". Dieses neue Projekt wird Tests für mein HelloWorld-Projekt enthalten, also nenne ich es HelloWorld.Tests. (Diese Namenskonvention ist übrigens nicht vorgeschrieben - ich hätte es auch anders nennen können.) Wenn ich auf OK klicke, erstellt Visual Studio ein zweites Projekt und beide werden nun im Projektmappen-Explorer aufgeführt, der ähnlich wie in Abbildung 1-1 aussieht.

Mit diesem Testprojekt soll sichergestellt werden, dass das Hauptprojekt das tut, was es soll. Ich bevorzuge die Art der Entwicklung, bei der du deine Tests schreibst, bevor du den zu testenden Code schreibst, also beginnen wir mit dem Testprojekt. Damit mein Testprojekt seine Aufgabe erfüllen kann, muss es Zugriff auf den Code im HelloWorld-Projekt haben. Visual Studio versucht nicht zu erraten, welche Projekte in einer Projektmappe von welchen anderen Projekten abhängen könnten. Auch wenn es hier nur zwei gibt, würde es höchstwahrscheinlich falsch raten, denn HelloWorld erzeugt ein ausführbares Programm, während Unit-Test-Projekte eine Bibliothek erzeugen. Die naheliegendste Vermutung wäre, dass das Programm von der Bibliothek abhängt, aber hier haben wir die etwas ungewöhnliche Anforderung, dass unsere Bibliothek (die eigentlich ein Testprojekt ist) Zugriff auf den Code in unserer Anwendung benötigt.

Ein Projekt in einem anderen referenzieren

Um Visual Studio die Beziehung zwischen diesen beiden Projekten mitzuteilen, klicke ich im Projektmappen-Explorer mit der rechten Maustaste auf den Knoten Abhängigkeiten des Projekts HelloWorld.Test und wähle den Menüpunkt Referenz hinzufügen. Daraufhin öffnet sich das Dialogfeld Referenzmanager, das du in Abbildung 1-5 sehen kannst. Auf der linken Seite wählst du die Art des Verweises aus - in diesem Fall möchte ich einen Verweis auf ein anderes Projekt in derselben Projektmappe einrichten, also habe ich den Abschnitt Projekte erweitert und die Projektmappe ausgewählt. In der Mitte werden alle anderen Projekte aufgelistet. In diesem Fall gibt es nur eines, also markiere ich den Eintrag HelloWorld und klicke auf OK.

The Reference Manager dialog
Abbildung 1-5. Das Dialogfeld Referenzmanager

Externe Bibliotheken referenzieren

So umfangreich die .NET-Klassenbibliothek auch sein mag, sie deckt nicht alle Eventualitäten ab. Es gibt Tausende von nützlichen Bibliotheken für .NET, viele davon sind kostenlos. Microsoft liefert immer mehr Bibliotheken separat von der .NET-Klassenbibliothek aus. Visual Studio unterstützt das Hinzufügen von Referenzen über das bereits erwähnte NuGet-System. Das Beispiel verwendet es bereits, obwohl wir Microsofts eigenes Testframework "MSTest" gewählt haben, das nicht in .NET integriert ist. (In der Regel brauchst du keine Unit-Testing-Dienste zur Laufzeit, daher ist es nicht nötig, sie in die Klassenbibliothek zu integrieren, die mit der Plattform geliefert wird). Wenn du im Projektmappen-Explorer den Knoten Abhängigkeiten für das Projekt HelloWorld.Tests und dann den untergeordneten Knoten NuGet erweiterst, siehst du verschiedene NuGet-Pakete, wie in Abbildung 1-6 dargestellt. (Möglicherweise siehst du höhere Versionsnummern, da diese Bibliotheken ständig weiterentwickelt werden).

Du siehst vier testbezogene Pakete, die alle als Teil der Testprojektvorlage von Visual Studio für uns hinzugefügt wurden. NuGet ist ein paketbasiertes System. Anstatt also einen Verweis auf eine einzelne DLL hinzuzufügen, fügst du einen Verweis auf ein Paket hinzu, das mehrere DLLs und alle anderen Dateien enthalten kann, die für die Verwendung der Bibliothek benötigt werden.

NuGet references
Abbildung 1-6. NuGet-Referenzen

Das öffentliche Repository der Pakete, das Microsoft auf der Website http://nuget.org betreibt, enthält Kopien aller Bibliotheken, die Microsoft nicht direkt in die .NET-Klassenbibliothek aufnimmt, aber dennoch vollständig unterstützt. (Das hier verwendete Testing Framework ist ein Beispiel dafür. Das ASP.NET Core Web-Framework ist ein weiteres.) Dieses zentrale NuGet-Repository ist nicht nur für Microsoft gedacht. Jeder kann dort Pakete zur Verfügung stellen, daher findest du hier die meisten kostenlosen .NET-Bibliotheken.

Visual Studio kann im Haupt-NuGet-Repository suchen. Wenn du mit der rechten Maustaste auf ein Projekt oder den Knoten Abhängigkeiten klickst und NuGet-Pakete verwalten auswählst, öffnet sich das Fenster NuGet-Paketmanager, das in Abbildung 1-7 dargestellt ist. Auf der linken Seite findest du eine Liste der Pakete aus dem NuGet Repository. Wenn du oben auf Installiert klickst, werden nur die Pakete angezeigt, die du bereits verwendest. Wenn du auf Durchsuchen klickst, werden standardmäßig die gängigen verfügbaren Pakete angezeigt, aber es gibt auch ein Textfeld, mit dem du nach bestimmten Bibliotheken suchen kannst.

NuGet Package Manager
Abbildung 1-7. NuGet Paket Manager

Es ist auch möglich, deine eigenen NuGet-Repositories zu hosten. Viele Unternehmen betreiben zum Beispiel Repositories hinter ihrer Firewall, um intern entwickelte Pakete anderen Mitarbeitern zur Verfügung zu stellen, ohne sie öffentlich zugänglich machen zu müssen. Die Website https://myget.org ist auf Online-Hosting spezialisiert, und privates Paket-Hosting ist eine Funktion von Microsofts Azure DevOps und auch von GitHub. Oder du kannst ein Repository einfach auf einem lokal zugänglichen Dateisystem hosten. Du kannst NuGet so konfigurieren, dass es zusätzlich zu dem öffentlichen Repository eine beliebige Anzahl von Repositories durchsucht.

Eine sehr wichtige Funktion von NuGet-Paketen ist, dass sie Abhängigkeiten von anderen Paketen angeben können. Wenn du dir zum Beispiel das Paket Microsoft.NET.Test.Sdk in Abbildung 1-6 ansiehst, kannst du an dem kleinen Dreieck daneben erkennen, dass sein Baumknoten erweiterbar ist. Wenn du ihn aufklappst, siehst du, dass er von einigen anderen Paketen abhängt, darunter Microsoft.CodeCoverage. Da Pakete ihre Abhängigkeiten beschreiben, kann Visual Studio automatisch alle Pakete abrufen, die du benötigst.

Einen Einheitstest schreiben

Jetzt muss ich einen Test schreiben. Visual Studio hat mir für den Anfang eine Testklasse in einer Datei namens UnitTest1.cs zur Verfügung gestellt. 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 sollten. Wie du anhand der gewählten Projektnamen wahrscheinlich schon erraten hast, wird mein Programm nur ein Verhalten haben: Es wird eine "Hallo, Welt!"-Nachricht anzeigen, wenn es läuft. Ich benenne also die Quelldatei UnitTest1.cs in WhenProgramRuns.cs um. Dieser Test soll überprüfen, ob das Programm die gewünschte Nachricht anzeigt, wenn es läuft. Der Test selbst ist sehr einfach, aber leider ist es etwas komplizierter, diesen speziellen Test zu starten. Beispiel 1-1 zeigt die gesamte Quelldatei; der Test steht am Ende, in Fettdruck.

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

namespace HelloWorld.Tests
{
    [TestClass]
    public class WhenProgramRuns
    {
        private string _consoleOutput;

        [TestInitialize]
        public void Initialize()
        {
            var w = new System.IO.StringWriter();
            Console.SetOut(w);

            Program.Main(new string[0]);

            _consoleOutput = w.GetStringBuilder().ToString().Trim();
        }

        [TestMethod]
        public void SaysHelloWorld()
        {
            Assert.AreEqual("Hello, world!", _consoleOutput);
        }
    }
}

Ich werde die einzelnen Funktionen in dieser Datei erklären, sobald ich das Programm selbst gezeigt habe. Der interessanteste Teil dieses Beispiels ist die Methode SaysHelloWorld, die ein bestimmtes Verhalten unseres Programms definiert. Der Test legt fest, dass die Ausgabe des Programms die Nachricht "Hallo, Welt" sein soll. Wenn das nicht der Fall ist, meldet der Test einen Fehler. Der Test selbst ist erfreulich einfach, aber der Code, der den Test einrichtet, ist ein wenig umständlich. Das Problem dabei ist, dass das obligatorische erste Beispiel, das in allen Programmierbüchern zu finden ist, nicht sehr gut für Unit-Tests einzelner Klassen oder Methoden geeignet ist, weil man nur das gesamte Programm testen kann. Wir wollen überprüfen, ob das Programm eine bestimmte Meldung in die Konsole schreibt. In einer realen Anwendung könntest du eine Art Abstraktion für die Ausgabe entwickeln, und deine Unit-Tests würden eine gefälschte Version dieser Abstraktion für Testzwecke bereitstellen. Ich möchte jedoch, dass meine Anwendung (die in Beispiel 1-1 lediglich getestet wird) dem Standardbeispiel "Hallo, Welt!" entspricht. Um das Hauptprogramm nicht zu kompliziert zu machen, habe ich dafür gesorgt, dass mein Test die Konsolenausgabe abfängt, damit ich überprüfen kann, ob das Programm das anzeigt, was beabsichtigt war.(In Kapitel 15 beschreibe ich die Funktionen, die ich dafür aus dem Namensraum System.IO verwende).

Es gibt noch eine zweite Herausforderung. Normalerweise testet ein Unit-Test per Definition einen isolierten und meist kleinen Teil des Programms. Aber in diesem Fall ist das Programm so einfach, dass es nur eine Funktion gibt, die von Interesse ist, und diese Funktion wird ausgeführt, wenn wir das Programm starten. Das bedeutet, dass mein Test den Einstiegspunkt des Programms aufrufen muss. Dazu hätte ich mein HelloWorld-Programm in einem ganz neuen Prozess starten können, aber das Abfangen der Ausgabe wäre etwas komplizierter gewesen als das prozessinterne Abfangen in Beispiel 1-1. Stattdessen rufe ich einfach den Einstiegspunkt des Programms direkt auf. In einer C#-Anwendung ist der Einstiegspunkt normalerweise eine Methode namens Main, die in einer Klasse namens Program definiert ist. Beispiel 1-2 zeigt die entsprechende Zeile aus Beispiel 1-1, wobei ein leeres Array übergeben wird, um zu simulieren, dass das Programm ohne Kommandozeilenargumente ausgeführt wird.

Beispiel 1-2. Aufrufen einer Methode
Program.Main(new string[0]);

Leider gibt es dabei ein Problem. Der Einstiegspunkt eines Programms ist normalerweise nur für die Laufzeit zugänglich - er ist ein Implementierungsdetail deines Programms und es gibt normalerweise keinen Grund, ihn öffentlich zugänglich zu machen. Hier mache ich jedoch eine Ausnahme, denn dort befindet sich der einzige Code in diesem Beispiel. Damit der Code kompiliert werden kann, müssen wir also eine Änderung an unserem Hauptprogramm vornehmen. Beispiel 1-3 zeigt den entsprechenden Code aus der Datei Program.cs im Projekt HelloWorld. (Ich zeige das Ganze in Kürze.)

Beispiel 1-3. Den Programmeinstiegspunkt zugänglich machen
public class Program
{
    public static void Main(string[] args)
    {
...

Ich habe das Schlüsselwort public am Anfang von zwei Zeilen hinzugefügt, um den Code für den Test zugänglich zu machen, damit Beispiel 1-1 kompiliert werden kann. Es gibt andere Möglichkeiten, wie ich das hätte erreichen können. Ich hätte die Klasse so lassen können, wie sie ist, die Methode internal einfügen und dann InternalsVisibleToAttribute auf mein Programm anwenden können, um den Zugriff nur für die Testsuite zu ermöglichen. Aber interner Schutz und Attribute auf Baugruppenebene sind Themen für spätere Kapitel (Kapitel 3 bzw. 14), also habe ich mich entschieden, es für dieses erste Beispiel einfach zu halten. Den alternativen Ansatz werde ich in Kapitel 14 vorstellen.

Jetzt kann ich meinen Test ausführen. Dazu öffne ich den Unit Test Explorer von Visual Studio über den Menüpunkt Test→Windows→Test Explorer. Dann erstelle ich das Projekt mit dem Menü Build→Build Solution. Danach zeigt der Unit Test Explorer eine Liste aller in der Lösung definierten Unit-Tests an. Er findet meinen SaysHelloWorld Test, wie du in Abbildung 1-8 sehen kannst. Wenn ich auf die Schaltfläche Alle ausführen (der Doppelpfeil oben links) klicke, wird der Test ausgeführt, der fehlschlägt, weil wir bisher nur den Test geschrieben, aber noch nichts an unserem Hauptprogramm geändert haben. Du kannst die Fehlermeldung am unteren Rand von Abbildung 1-8 sehen. Er besagt, dass er eine "Hello, world!"-Nachricht erwartet hat, aber die tatsächliche Konsolenausgabe war anders. (Zugegebenermaßen nicht sehr viel - Visual Studio hat tatsächlich Code zu meiner Konsolenanwendung hinzugefügt, der eine Nachricht anzeigt. Aber er enthält nicht das Komma, das mein Test verlangt, und die w hat die falsche Groß-/Kleinschreibung).

Unit Test Explorer
Abbildung 1-8. Unit Test Explorer

Es ist also an der Zeit, unser HelloWorld-Programm anzuschauen und den Code zu korrigieren. Als ich das Projekt erstellt habe, hat Visual Studio verschiedene Dateien erzeugt, darunter Program.cs, die den Einstiegspunkt des Programms enthält. Beispiel 1-4 zeigt diese Datei, einschließlich der Änderungen, die ich in Beispiel 1-3 vorgenommen habe. Ich werde jedes Element der Reihe nach erklären, da es eine nützliche Einführung in einige wichtige Elemente der C#-Syntax und -Struktur darstellt.

Beispiel 1-4. Programm.cs
using System;

namespace HelloWorld
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Die Datei beginnt mit einer using-Direktive. Diese ist zwar optional, aber fast alle Quelldateien enthalten eine oder mehrere und teilen dem Compiler mit, welche Namensräume wir verwenden möchten, was die Frage aufwirft: Was ist ein Namensraum?

Namensräume

Namensräume bringen Ordnung und Struktur in das, was sonst ein furchtbares Durcheinander wäre. Die .NET-Klassenbibliothek enthält eine große Anzahl von Klassen, und es gibt noch viel mehr Klassen in Bibliotheken von Drittanbietern, ganz zu schweigen von den Klassen, die du selbst schreibst. Es gibt zwei Probleme, die beim Umgang mit so vielen benannten Entitäten auftreten können. Erstens wird es schwierig, die Eindeutigkeit zu garantieren, wenn nicht alles einen sehr langen Namen hat oder die Namen Teile von zufälligem Kauderwelsch enthalten. 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 mit vielen Tausenden von Dingen zu finden. Namensräume lösen diese beiden Probleme.

Die meisten .NET-Typen sind in einem Namensraum definiert. Die von Microsoft bereitgestellten Typen haben eigene Namensräume. Wenn die Typen Teil von .NET sind, beginnen die enthaltenen Namensräume mit System, und wenn sie Teil einer Microsoft-Technologie sind, die nicht zum Kern von .NET gehört, beginnen sie normalerweise mit Microsoft. Bibliotheken von anderen Anbietern beginnen in der Regel mit dem Firmennamen, während Open-Source-Bibliotheken oft ihren Projektnamen verwenden. Du bist nicht gezwungen, deine eigenen Typen in Namensräume einzubinden, aber es wird empfohlen, dies zu tun. C# behandelt System nicht als speziellen Namespace, also hält dich nichts davon ab, ihn für deine eigenen Typen zu verwenden, aber wenn du nicht gerade einen Beitrag zur .NET-Klassenbibliothek schreibst, den du als Pull-Request bei https://github.com/dotnet/corefx einreichst , ist das keine gute Idee, weil es andere Entwickler eher verwirren wird. Für deinen eigenen Code solltest du etwas Unverwechselbares wählen, z. B. den Namen deines Unternehmens oder deines Projekts.

Der Namensraum gibt normalerweise einen Hinweis auf den Zweck des Typs. So finden sich z. B. alle Typen, die sich auf den Umgang mit Dateien beziehen, im Namensraum System.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 nicht nur Typen. Er enthält 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 von regulären Ausdrücken suchst, könntest du die verfügbaren Namensräume durchsehen und dabei den Namensraum System.Text entdecken. Wenn du dort nachschaust, 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 eher 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 haben trotzdem unterschiedliche Namen. Der vollständige Name der Klasse Capture lautet System.Text.RegularExpressions.Capture, und der vollständige Name deiner Klasse würde den enthaltenen Namensraum enthalten (z. B. SpiffingSoftworks.Imaging.Capture).

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 nicht tun. Hier kommt die Direktive using am Anfang von Beispiel 1-4 ins Spiel. In diesem einfachen Beispiel gibt es zwar nur eine, aber es ist üblich, hier eine Liste von Direktiven zu sehen. Diese geben die Namensräume der Typen an, die eine Quelldatei verwenden soll. Normalerweise wirst du diese Liste so bearbeiten, dass sie den Anforderungen deiner Datei entspricht. In diesem Beispiel hat Visual Studio using System; hinzugefügt, als ich das Projekt erstellt habe. In verschiedenen Kontexten wählt es unterschiedliche Sets aus. 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.

Mit using Deklarationen wie diesen kannst du einfach den kurzen, unqualifizierten Namen für eine Klasse verwenden. Die Codezeile, die dafür sorgt, dass mein HelloWorld-Beispiel seine Aufgabe erfüllt, verwendet die Klasse System.Console, aber wegen der ersten Direktive using kann ich sie einfach Console nennen. Da dies die einzige Klasse ist, die ich verwende, muss ich in meinem Hauptprogramm keine weiteren using Direktiven einfügen.

Hinweis

Vorhin hast du gesehen, dass die Referenzen eines Projekts beschreiben, welche Bibliotheken es verwendet. 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 Framework Version der Klassenbibliothek enthält eine System.Core.dll Datei, aber es gibt keinen System.Core Namensraum. Es ist also notwendig, dem Compiler mitzuteilen, von welchen Bibliotheken dein Projekt abhängt, und welche Namensräume eine bestimmte Quelldatei verwendet. In Kapitel 12 werden wir uns die Art und Struktur von Bibliotheksdateien genauer ansehen.

Auch bei Namensräumen kann es zu Mehrdeutigkeiten kommen. Du könntest zwei Namensräume verwenden, 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. Beispiel 1-5 verwendet Aliase, um einen Konflikt zu lösen, der mir schon öfter begegnet ist: Das UI-Framework von .NET, die Windows Presentation Foundation (WPF), definiert eine Klasse Path für die Arbeit mit Bézier-Kurven, Polygonen und anderen Formen, aber es gibt auch eine Klasse Path 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-5 zeigt, kannst du für beide Namensräume eindeutige Aliase definieren.

Beispiel 1-5. Mehrdeutigkeit mit Aliasen auflösen
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.

Um auf unser HelloWorld-Beispiel zurückzukommen: Direkt nach den using -Direktiven folgt eine Namensraum-Deklaration. Während die using Direktiven angeben, welche Namensräume unser Code verwenden wird, gibt eine Namensraumdeklaration den Namensraum an, in dem sich unser eigener Code befindet. Beispiel 1-6 zeigt den entsprechenden Code aus Beispiel 1-4. Es folgt eine öffnende Klammer ({). Alles, was zwischen dieser und der schließenden Klammer am Ende der Datei steht, befindet sich im Namensraum HelloWorld. Übrigens kannst du dich ohne Qualifikation auf Typen in deinem eigenen Namensraum beziehen, ohne dass du eine using Direktive brauchst. Deshalb hat der Testcode in Beispiel 1-1 keine using HelloWorld; Direktive - er hat implizit Zugang zu diesem Namespace, weil sein Code in einer namespace HelloWorld.Tests Deklaration steht.

Beispiel 1-6. Namensraum-Deklaration
namespace HelloWorld
{

Visual Studio erzeugt eine Namespace-Deklaration mit demselben Namen wie dein Projekt in den Quelldateien, die es beim Erstellen eines neuen Projekts hinzufügt. Du bist nicht verpflichtet, diesen Namen beizubehalten - ein Projekt kann eine beliebige Mischung von Namensräumen enthalten, und es steht dir frei, die Namensraum-Deklaration zu bearbeiten. Wenn du jedoch in deinem Projekt durchgängig einen anderen Namen als den Projektnamen verwenden möchtest, solltest du Visual Studio dies mitteilen, denn nicht nur die erste Datei, Program.cs, erhält diese generierte Deklaration. Standardmäßig fügt Visual Studio jedes Mal, wenn du eine neue Datei hinzufügst, eine Namespace-Deklaration hinzu, die auf deinem Projektnamen basiert. Du kannst einen anderen Namensraum für neue Dateien verwenden, indem du die Eigenschaften des Projekts bearbeitest. Wenn du im Projektmappen-Explorer mit der rechten Maustaste auf den Projektknoten klickst und Eigenschaften auswählst, werden die Eigenschaften des Projekts geöffnet. Dort findest du ein Textfeld "Standard-Namensraum". Es wird das verwendet, was du dort für die Deklaration der Namensräume neuer Dateien eingibst. (Bestehende Dateien werden jedoch nicht geändert.) Damit wird der .csproj-Datei die Eigenschaft <RootNamespace> hinzugefügt.

Verschachtelte Namensräume

Wie du bereits gesehen hast, verschachtelt die .NET-Klassenbibliothek ihre Namensräume, und zwar manchmal ziemlich umfangreich. Wenn du nicht gerade ein triviales Beispiel erstellst, wirst du normalerweise deine eigenen Namensräume verschachteln. Es gibt zwei Möglichkeiten, dies zu tun. Du kannst Namensraum-Deklarationen verschachteln, wie Beispiel 1-7 zeigt.

Beispiel 1-7. 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-8 zeigt. Dies ist die am häufigsten verwendete Methode.

Beispiel 1-8. 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-7 und 1-8 bräuchte keine explizite Qualifikation oder using Direktiven, um Typen aus dem MyApp.Storage oder MyApp Namensraum zu verwenden.

Wenn du verschachtelte Namensräume definierst, wird in der Regel eine entsprechende Ordnerhierarchie erstellt. Wenn du, wie du gesehen hast, ein Projekt mit dem Namen MyApp erstellst, legt Visual Studio neue Klassen standardmäßig im Namespace MyApp ab, wenn du sie dem Projekt hinzufügst. Wenn du jedoch im Projekt einen neuen Ordner mit dem Namen Speicherung erstellst (was du im Projektmappen-Explorer tun kannst), legt Visual Studio alle neuen Klassen, die du in diesem Ordner 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 muss den Namensraum nicht mit deiner Ordnerhierarchie abgleichen. Aber da Visual Studio diese Konvention unterstützt, ist es einfacher, wenn du sie befolgst.

Klassen

Innerhalb der Namespace-Deklaration definiert meine Program.cs-Datei eine Klasse. Beispiel 1-9 zeigt diesen Teil der Datei (der die public Schlüsselwörter enthält, die ich zuvor hinzugefügt habe). Nach dem Schlüsselwort class folgt der Name, und natürlich ist der vollständige Name des Typs tatsächlich HelloWorld.Program, da dieser Code innerhalb der Namespace-Deklaration steht. Wie du siehst, verwendet C# geschweifte Klammern ({}), um alles Mögliche abzugrenzen - das haben wir schon bei den Namensräumen gesehen, und hier kannst du das Gleiche bei der Klasse und der darin enthaltenen Methode sehen.

Beispiel 1-9. Eine Klasse mit einer Methode
public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

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 den Einstiegspunkt des Programms zu dienen. In Kapitel 3 werden wir weitere interessante Einsatzmöglichkeiten für Klassen kennenlernen.

Programm Einstiegspunkt

Standardmäßig sucht der C#-Compiler nach einer Methode namens Main und verwendet diese automatisch als Einstiegspunkt. Wenn du es wirklich willst, kannst du dem Compiler sagen, dass er eine andere Methode verwenden soll, aber die meisten Programme halten sich an die Konvention. Unabhängig davon, ob du den Einstiegspunkt per Konfiguration oder Konvention festlegst, muss die Methode bestimmte Anforderungen erfüllen, die in Beispiel 1-9 deutlich werden.

Der Programmeinstiegspunkt muss eine statische Methode sein, d.h. es ist nicht notwendig, eine Instanz des enthaltenen Typs (in diesem FallProgram) zu erzeugen, um die Methode aufzurufen. Sie muss nichts zurückgeben, wie das Schlüsselwort void hier zeigt. Wenn du möchtest, kannst du stattdessen int zurückgeben, wodurch das Programm einen Exit-Code zurückgeben kann, den das Betriebssystem meldet, wenn das Programm beendet wird. (Sie kann auch entweder Task oder Task<int> zurückgeben, was es dir ermöglicht, sie zu einer async Methode zu machen, wie in Kapitel 17 beschrieben). Und die Methode darf entweder gar keine Argumente annehmen (was durch ein leeres Klammerpaar nach dem Methodennamen angezeigt wird) oder sie kann, wie in Beispiel 1-9, ein einziges Argument annehmen: ein Array von Textstrings, das die Befehlszeilenargumente enthält.

Hinweis

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.

Auf die Methodendeklaration folgt der Methodenrumpf, der in diesem Fall Code enthält, der fast genau das ist, was wir wollen. Wir haben uns nun alles angesehen, was Visual Studio in dieser Datei für uns generiert hat. Jetzt müssen wir nur noch den Code in den geschweiften Klammern ändern, die den Methodenrumpf begrenzen. Erinnere dich daran, dass unser Test fehlschlägt, weil unser Programm die einzige Anforderung nicht erfüllt: eine bestimmte Meldung auf der Konsole auszugeben. Dafür ist die in Beispiel 1-10 gezeigte Codezeile innerhalb des Methodenrumpfes erforderlich. Das ist fast genau das, was schon da ist, nur mit einem zusätzlichen Komma und einem Kleinbuchstaben w.

Beispiel 1-10. Eine Nachricht anzeigen
Console.WriteLine("Hello, world!");

Wenn ich nun die Tests erneut ausführe, zeigt der Unit Test Explorer ein Häkchen bei meinem Test an und meldet, dass alle Tests bestanden wurden. Der Code funktioniert also offensichtlich. Und wir können das informell überprüfen, indem wir das Programm ausführen. Das kannst du über das Debug-Menü von Visual Studio tun. Mit der Option Debugging starten wird das Programm im Debugger ausgeführt. Wenn du das Programm auf diese Weise ausführst (was du auch mit dem Tastaturkürzel F5 tun kannst), öffnet sich ein Konsolenfenster, in dem die traditionelle Meldung angezeigt wird.

Einheitstests

Jetzt, wo das Programm funktioniert, möchte ich zum ersten Code zurückkehren, den ich geschrieben habe, dem Test, denn diese Datei veranschaulicht einige C#-Funktionen, die das Hauptprogramm nicht hat. Wenn du zu Beispiel 1-1 zurückgehst, beginnt es ziemlich ähnlich wie das Hauptprogramm: Wir haben einige using Direktiven und dann eine Namespace-Deklaration, wobei der Namespace dieses Mal HelloWorld.Tests ist, passend zum Namen des Testprojekts. Aber die Klasse sieht anders aus. Beispiel 1-11 zeigt den relevanten Teil von Beispiel 1-1.

Beispiel 1-11. Testklasse mit Attribut
[TestClass]
public class WhenProgramRuns
{

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-12 zeigt die entsprechenden Auszüge aus Beispiel 1-1. Der Testläufer führt alle mit [TestInitialize] gekennzeichneten Methoden für jeden Test, den die Klasse enthält, einmal aus, und zwar bevor er die eigentliche Testmethode selbst ausführt. Und wie du sicher schon vermutet hast, teilt das Attribut [TestMethod] dem Test Runner mit, welche Methoden Tests darstellen.

Beispiel 1-12. Kommentierte Methoden
[TestInitialize]
public void Initialize()
...

[TestMethod]
public void SaysHelloWorld()
...

In Beispiel 1-1 gibt es noch eine weitere Besonderheit: Der Klasseninhalt beginnt mit einem Feld, wie in Beispiel 1-13 gezeigt. Felder enthalten Daten. In diesem Fall speichert die Methode Initialize die Konsolenausgabe, die sie während der Ausführung des Programms aufnimmt, in diesem Feld _consoleOutput, wo sie für Testmethoden zur Verfügung steht. Dieses Feld wurde als private markiert, was bedeutet, dass es nur für die enthaltende Klasse bestimmt ist. Der C#-Compiler erlaubt nur dem Code, der sich in derselben Klasse befindet, den Zugriff auf diese Daten.

Beispiel 1-13. Ein Feld
private string _consoleOutput;

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, hatte jedes Projekt nur eine Quelldatei, die von Interesse war. Beide hatten eine ähnliche Struktur. Sie begannen jeweils mit using Direktiven, die angaben, welche Typen die Datei verwendet. Eine Namespace-Deklaration gab den Namespace an, den die Datei bevölkert, und dieser enthielt eine Klasse, die eine oder mehrere Methoden oder andere Mitglieder, wie z. B. Felder, enthielt.

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 galt auch für das vorherige plattformübergreifende .NET-Angebot von Microsoft. Im Jahr 2008 stellte Microsoft Silverlight 2.0 vor, mit dem C# in Browsern unter Windows und macOS ausgeführt werden konnte. Silverlight kämpfte einen aussichtslosen Kampf gegen die immer besseren Möglichkeiten und die universelle Reichweite von HTML5 und JavaScript, aber die Tatsache, dass es ein Closed-Source-Programm war, dürfte ihm nicht geholfen haben.

2 Microsofts erster Satz von .NET-Erweiterungen für C++ ähnelte eher dem normalen C++. Es stellte sich heraus, dass es verwirrend war, die bestehende Syntax für etwas zu verwenden, das sich stark von normalem C++ unterschied. Deshalb hat Microsoft das erste System (Managed C++) zugunsten der neueren, markanteren Syntax, die C++/CLI genannt wird, abgeschafft.

3.NET Native ist eine Ausnahme: Es unterstützt kein Laufzeit-JIT und bietet daher keine abgestufte Kompilierung.

4 Seltsamerweise hat diese erste, UWP-unterstützende Version aus dem Jahr 2015 offenbar nie eine offizielle Versionsnummer erhalten. Die .NET Core 1.0 Version ist auf Juni 2016 datiert, also etwa ein Jahr später.

Get C# 8.0 programmieren 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.