Überblick über C#

C# (Aussprache „C Sharp“) ist eine moderne, objektorientierte und typsichere Programmiersprache. C# ermöglicht Entwicklern das Erstellen zahlreicher sicherer und robuster Anwendungen, die in .NET ausgeführt werden. C# hat seine Wurzeln in der C-Sprachenfamilie und ist Programmierern, die mit C, C++, Java und JavaScript arbeiten, sofort vertraut. Diese Einführung bietet einen Überblick über die wichtigsten Komponenten der Sprache in C# 8 und früheren Versionen. Wenn Sie die Sprache anhand von interaktiven Beispielen kennenlernen möchten, arbeiten Sie die Tutorials auf der Seite Einführung in C# durch.

C# ist eine objektorientierte, komponentenorientierte Programmiersprache. C# bietet Sprachkonstrukte zur direkten Unterstützung dieser Konzepte, was C# zu einer natürlichen Sprache macht, in der Softwarekomponenten erstellt und verwendet werden. Seit Veröffentlichung wurden C# Features hinzugefügt, um neue Workloads und Methoden zur Gestaltung von Software zu unterstützen. Im Kern ist C# eine objektorientierte Programmiersprache. Sie definieren Typen und deren Verhalten.

Mehrere C#-Features helfen bei der Erstellung stabiler und dauerhafter Anwendungen. Die Garbage Collection gibt Arbeitsspeicher automatisch frei, der von nicht erreichbaren, nicht verwendeten Objekten belegt wird. Nullable-Typen bieten Schutz vor Variablen, die nicht auf zugeordnete Objekte verweisen. Die Ausnahmebehandlung bietet einen strukturierten und erweiterbaren Ansatz zur Fehlererkennung und Wiederherstellung. Lambdaausdrücke unterstützen funktionale Programmiertechniken. Die Language Integrated Query-Syntax (LINQ) erstellt ein gängiges Muster für das Arbeiten mit Daten aus einer beliebigen Quelle. Dank Sprachunterstützung für asynchrone Vorgänge wird eine Syntax für den Aufbau verteilter Systeme bereitgestellt. C# bietet ein einheitliches Typsystem. Alle C#-Typen, einschließlich primitiver Typen wie int und double, erben von einem einzelnen object-Stammtyp. Allen Typen teilen sich eine Reihe allgemeiner Vorgänge. Werte jeglicher Art können einheitlich gespeichert, transportiert und bearbeitet werden. Darüber hinaus unterstützt C# benutzerdefinierte Verweis- und Werttypen. C# ermöglicht die dynamische Zuteilung von Objekten und die Inlinespeicherung schlanker Strukturen. C# unterstützt generische Methoden und Typen, die eine bessere Typsicherheit und Leistung bieten. C# stellt Iteratoren bereit, mit denen Implementierer von Auflistungsklassen benutzerdefinierte Verhaltensweisen für Clientcode definieren können.

In C# spielt die Versionsverwaltung eine wichtige Rolle, damit Programme und Bibliotheken im Laufe der Zeit kompatibel weiterentwickelt werden können. Zu den Aspekten der Entwicklung von C#, die direkt von Überlegungen bei der Versionskontrolle beeinflusst wurden, gehören die separaten virtual- und override-Modifizierer, die Regeln für die Überladungsauflösung und die Unterstützung für explizite Schnittstellenmember-Deklarationen.

.NET-Architektur

C#-Programme werden auf Grundlage von .NET ausgeführt, ein virtuelles Ausführungssystem namens Common Language Runtime (CLR) sowie Klassenbibliotheken. Die CLR ist die Implementierung der Common Language Infrastructure (CLI) von Microsoft, ein internationaler Standard. Die CLI ist die Grundlage für das Erstellen von Ausführungs- und Entwicklungsumgebungen, in denen Sprachen und Bibliotheken nahtlos zusammenarbeiten.

Der in C# geschriebene Quellcode wird in eine Zwischensprache kompiliert, die konform mit der CLI-Spezifikation ist. Der IL-Code wird zusammen mit Ressourcen wie z. B. Bitmaps und Zeichenfolgen in einer Assembly gespeichert, die normalerweise die Erweiterung .dll aufweist. Eine Assembly enthält ein Manifest, das Informationen über die Typen, die Version und die Kultur der Assembly bereitstellt.

Wenn das C#-Programm ausgeführt wird, wird die Assembly in die CLR geladen. Die CLR konvertiert den IL-Code mithilfe der JIT-Kompilierung (Just-In-Time) in native Computeranweisungen. Die CLR stellt weitere Dienste zur automatischen Garbage Collection, Ausnahmebehandlung und Ressourcenverwaltung bereit. Der über die CLR ausgeführte Code wird manchmal als „verwalteter Code“ bezeichnet. Demgegenüber wird „nicht verwalteten Code“ in die native Maschinensprache für eine bestimmte Plattform kompiliert.

Eines der wichtigsten Features in .NET ist die Sprachinteroperabilität. Der vom C#-Compiler erzeugte IL-Code entspricht dem allgemeinen Typsystem (CTS, Common Type Specification). Der über C# generierte IL-Code kann mit Code interagieren, der über die .NET-Versionen von F#, Visual Basic oder C++ generiert wurde. Es gibt mehr als 20 weitere CTS-kompatible Sprachen. Eine einzelne Assembly kann mehrere Module enthalten, die in verschiedenen .NET-Sprachen geschrieben wurden. Die Typen können aufeinander verweisen, als wären sie in der gleichen Sprache geschrieben.

Zusätzlich zu den Laufzeitdiensten enthält .NET auch umfangreiche Bibliotheken. Diese Bibliotheken unterstützen viele verschieden Workloads. Sie sind in Namespaces organisiert, die eine große Bandbreite nützlicher Funktionen bereitstellen. Die Bibliotheken beinhalten alles von der Dateiein- und -ausgabe über die Bearbeitung von Zeichenfolgen, XML-Analyse und Webanwendungs-Frameworks bis hin zu Windows Forms-Steuerelementen. Eine typische C#-Anwendung verwendet für die Ausführung von Routinevorgängen ausgiebig die .NET-Klassenbibliothek.

Weitere Informationen zu .NET finden Sie in der Übersicht über .NET.

Hello World

Das Programm „Hello, World“ wird für gewöhnlich zur Einführung einer Programmiersprache verwendet. Hier ist es in C#:

using System;

class Hello
{
    static void Main()
    {
        Console.WriteLine("Hello, World");
    }
}

Das Programm „Hello, World“ wird mit einer using-Richtlinie gestartet, die auf den System-Namespace verweist. Namespaces bieten eine hierarchische Möglichkeit zum Organisieren von C#-Programmen und -Bibliotheken. Namespaces enthalten Typen und andere Namespaces. Beispiel: Der System-Namespace enthält eine Reihe von Typen, wie etwa die Console-Klasse, auf die im Programm verwiesen wird, und eine Reihe anderer Namespaces, wie etwa IO und Collections. Eine using-Richtlinie, die auf einen bestimmten Namespace verweist, ermöglicht die nicht qualifizierte Nutzung der Typen, die Member dieses Namespace sind. Aufgrund der using-Direktive kann das Programm Console.WriteLine als Abkürzung für System.Console.WriteLine verwenden.

Die Hello-Klasse, die vom Programm „Hello, World“ deklariert wird, verfügt über einen einzelnen Member: die Main-Methode. Die Main-Methode wird mit dem Modifizierer static deklariert. Auch wenn Instanzmethoden mit dem Schlüsselwort this auf eine bestimmte einschließende Objektinstanz verweisen können, agieren statische Methoden ohne Verweis auf ein bestimmtes Objekt. Gemäß Konvention fungiert eine statische Methode mit der Bezeichnung Main als Einstiegspunkt eines C#-Programms.

Die Ausgabe des Programms wird anhand der WriteLine-Methode der Console-Klasse im System-Namespace generiert. Diese Klasse wird anhand der Standardklassenbibliotheken bereitgestellt, auf die standardmäßig automatisch vom Compiler verwiesen wird.

Typen und Variablen

Ein Typ definiert die Struktur und das Verhalten von allen Daten in C#. Die Deklaration eines Typs kann seine Member, seinen Basistyp, die Schnittstellen, die er implementiert, und die für diesen Typ zulässigen Operationen enthalten. Eine Variable ist eine Bezeichnung, die auf einen bestimmten Instanzentyp verweist.

Es gibt zwei Arten von Typen in C#: Werttypen und Verweistypen. Variablen von Werttypen enthalten ihre tatsächlichen Daten. Variablen von Verweistypen speichern hingegen Verweise auf ihre Daten – letztere werden als Objekte bezeichnet. Dank Verweistypen können zwei Variablen auf das gleiche Objekt verweisen. So können auf eine Variable angewendete Vorgänge das Objekt beeinflussen, auf das die andere Variable verweist. Bei Werttypen besitzen die Variablen jeweils eigene Kopien der Daten. Auf eine Variable angewendete Vorgänge können sich nicht auf die andere Variable auswirken (außer im Fall der Parametervariablen ref und out).

Ein Bezeichner ist ein Variablenname. Ein Bezeichner ist eine Sequenz von Unicode-Zeichen ohne Leerzeichen. Ein Bezeichner kann ein in C# reserviertes Wort sein, wenn ihm das Präfix @ vorangestellt ist. Die Verwendung eines reservierten Worts als Bezeichner kann nützlich sein, wenn Sie mit anderen Sprachen interagieren.

C#-Werttypen sind weiter unterteilt in einfache Typen, Enumerationstypen, Strukturtypen, Nullable-Werttypen und Tuple-Werttypen. C#-Verweistypen sind weiter unterteilt in Klassentypen, Schnittstellentypen, Arraytypen und Delegattypen.

Im Folgenden finden Sie eine Übersicht des C#-Typsystems.

C#-Programme verwenden Typdeklarationen, um neue Typen zu erstellen. Eine Typdeklaration gibt den Namen und die Member des neuen Typs an. Sechs Typkategorien von C# können von Benutzern definiert werden: Klassentypen, Strukturtypen, Schnittstellentypen, Enumerationstypen, Delegattypen und Tuple-Werttypen. Sie können außerdem record-Typen deklarieren, entweder record struct oder record class. Datensatztypen verfügen über vom Compiler synthetisierte Member. Sie verwenden Datensätze in erster Linie zum Speichern von Werten mit minimalem zugeordneten Verhalten.

  • Ein class-Typ definiert eine Datenstruktur, die Datenmember (Felder) und Funktionsmember (Methoden, Eigenschaften usw.) enthält. Klassentypen unterstützen einzelne Vererbung und Polymorphie. Dies sind Mechanismen, durch die abgeleitete Klassen erweitert und Basisklassen spezialisiert werden können.
  • Ein struct-Typ ähnelt einem Klassentyp, da er eine Struktur mit Datenmembern und Funktionsmembern darstellt. Im Gegensatz zu Klassen sind Strukturen Werttypen, die in der Regel keine Heapzuordnung erfordern. Strukturtypen unterstützen keine benutzerdefinierte Vererbung, und alle Strukturtypen erben implizit vom Typ object.
  • Ein interface-Typ definiert einen Vertrag als benannte Gruppe öffentlicher Member. Ein class- oder struct-Typ, der einen interface-Typ implementiert, muss Implementierungen der Member der Schnittstelle bereitstellen. Eine interface kann von mehreren Basisschnittstellen erben, und eine class oder struct kann mehrere Schnittstellen implementieren.
  • Ein delegate-Typ stellt Verweise auf Methoden mit einer bestimmten Parameterliste und dem Rückgabetyp dar. Delegate ermöglichen die Behandlung von Methoden als Entitäten, die Variablen zugewiesen und als Parameter übergeben werden können. Delegate werden analog zu Funktionstypen von funktionalen Sprachen bereitgestellt. Außerdem ähneln sie konzeptionell Funktionszeigern, die es in einigen anderen Sprachen gibt. Im Gegensatz zu Funktionszeigern sind Delegaten objektorientiert und typsicher.

Die Typen, class, struct, interface und delegate unterstützen Generics, wodurch sie mit anderen Typen parametrisiert werden können.

C# unterstützt ein- und mehrdimensionale Arrays aller beliebigen Typen. Im Gegensatz zu den oben aufgeführten Typen müssen Arraytypen nicht deklariert werden, bevor sie verwendet werden können. Stattdessen werden Arraytypen erstellt, indem hinter einen Typnamen eckige Klammern gesetzt werden. int[] ist beispielsweise ein eindimensionales Array aus int, int[,] ein zweidimensionales Array aus int, während int[][] ein eindimensionales Array aus eindimensionalen Arrays oder ein Jagged Array aus int ist.

Nullable-Typen erfordern keine getrennte Definition. Für jeden Non-Nullable-Typ T gibt es einen entsprechenden Nullable-Typ T?, der einen zusätzlichen Wert, null, enthalten kann. Beispielsweise ist int? ein Typ, der jeden 32-Bit-Ganzzahlwert oder den Wert null enthalten kann. string? ist ein Typ, der beliebige string-Typen oder den Wert null enthalten kann.

Das C#-Typsystem ist dahingehend vereinheitlicht, dass ein Wert eines beliebigen Typs als object behandelt werden kann. Jeder Typ in C# ist direkt oder indirekt vom object-Klassentyp abgeleitet, und object ist die ultimative Basisklasse aller Typen. Werte von Verweistypen werden als Objekte behandelt, indem die Werte einfach als Typ object angezeigt werden. Werte von Werttypen werden durch Ausführen von Boxing- und Unboxingvorgängen als Objekte behandelt. Im folgenden Beispiel wird ein int-Wert in ein object und wieder in einen int-Wert konvertiert.

int i = 123;
object o = i;    // Boxing
int j = (int)o;  // Unboxing

Wenn ein Wert eines Werttyps einem object-Verweis zugewiesen wird, wird eine „Box“ zugeordnet, die den Wert enthalten soll. Bei dieser Box handelt es sich um eine Instanz eines Verweistyps, und der Wert wird in diese Box kopiert. Wenn umgekehrt ein object-Verweis in einen Werttyp umgewandelt wird, wird überprüft, ob der object-Typ, auf den verwiesen wird, eine Box des korrekten Werttyps ist. Nach erfolgreicher Überprüfung wird der Wert in der Box zum Werttyp kopiert.

Aus dem einheitlichen C#-Typensystem resultiert, dass Werttypen „bei Nachfrage“ als object-Verweise behandelt werden. Aufgrund der Vereinheitlichung können Bibliotheken für allgemeine Zwecke, die den Typ object verwenden, mit allen Typen verwendet werden können, die von object abgeleitet werden, wozu sowohl Verweis- als auch Werttypen zählen.

Es gibt mehrere Arten von Variablen in C#, einschließlich Feldern, Arrayelementen, lokalen Variablen und Parametern. Variablen stellen Speicherorte dar. Jede Variable hat, wie nachstehend gezeigt, einen Typ, der bestimmt, welche Werte in der Variablen gespeichert werden können.

  • Nicht auf NULL festlegbarer Werttyp
    • Ein Wert genau dieses Typs
  • Auf NULL festlegbarer Werttyp
    • Ein null-Wert oder ein Wert genau dieses Typs
  • object
    • Ein null-Verweis, ein Verweis auf ein Objekt eines beliebigen Verweistyps oder ein Verweis auf einen geschachtelten Wert eines beliebigen Werttyps
  • Klassentyp
    • Ein null-Verweis, ein Verweis auf eine Instanz dieses Klassentyps oder ein Verweis auf eine Instanz einer Klasse, die von diesem Klassentyp abgeleitet ist
  • Schnittstellentyp
    • Ein null-Verweis, ein Verweis auf eine Instanz eines Klassentyps, der diesen Schnittstellentyp implementiert, oder ein Verweis auf einen geschachtelten Wert eines Werttyps, der diesen Schnittstellentyp implementiert
  • Arraytyp
    • Ein null-Verweis, ein Verweis auf eine Instanz dieses Arraytyps oder ein Verweis auf eine Instanz eines kompatiblen Arraytyps
  • Delegattyp
    • Ein null-Verweis oder ein Verweis auf eine Instanz eines kompatiblen Delegattyp

Programmstruktur

Die wichtigsten Organisationskonzepte in C# sind Programme, Namespaces, Typen, Member und Assemblys. Programme deklarieren Typen, die Member enthalten, und können in Namespaces organisiert werden. Klassen, Strukturen und Schnittstellen sind Beispiele von Typen. Felder, Methoden, Eigenschaften und Ereignisse sind Beispiele für Member. Wenn C#-Programme kompiliert werden, werden sie physisch in Assemblys gepackt. Assemblys haben in der Regel die Erweiterung .exe oder .dll, je nachdem, ob sie Anwendungen oder Bibliotheken implementieren.

Nehmen Sie als einfaches Beispiel eine Assembly, die den folgenden Code enthält:

namespace Acme.Collections;

public class Stack<T>
{
    Entry _top;

    public void Push(T data)
    {
        _top = new Entry(_top, data);
    }

    public T Pop()
    {
        if (_top == null)
        {
            throw new InvalidOperationException();
        }
        T result = _top.Data;
        _top = _top.Next;

        return result;
    }

    class Entry
    {
        public Entry Next { get; set; }
        public T Data { get; set; }

        public Entry(Entry next, T data)
        {
            Next = next;
            Data = data;
        }
    }
}

Der vollqualifizierte Name dieser Klasse ist Acme.Collections.Stack. Die Klasse enthält mehrere Member: ein Feld mit dem Namen _top, zwei Methoden mit dem Namen Push und Pop sowie eine geschachtelte Klasse mit dem Namen Entry. Die Entry-Klasse enthält weitere drei Member: eine Eigenschaft mit dem Namen Next, eine Eigenschaft mit dem Namen Data und einen Konstruktor. Stack ist eine generische Klasse. Sie hat einen Typparameter, T, der bei seiner Verwendung durch einen konkreten Typ ersetzt wird.

Ein Stapel ist eine FILO-Sammlung (First In, Last Out). Neue Elemente werden am Anfang des Stapels hinzugefügt. Das Entfernen eines Elements erfolgt von oben aus dem Stapel. Im vorherigen Beispiel wird der Typ Stack deklariert, der den Speicher und das Verhalten für einen Stapel definiert. Sie können eine Variable deklarieren, die auf eine Instanz des Typs Stack verweist, um diese Funktionalität zu verwenden.

Assemblys enthalten ausführbaren Code in Form von Zwischensprachenanweisungen (Intermediate Language, IL) und symbolischen Informationen in Form von Metadaten. Vor der Ausführen konvertiert der JIT-Compiler (Just-In-Time) der .NET Common Language Runtime den IL-Code in einer Assembly in prozessorspezifischen Code.

Da eine Assembly eine selbstbeschreibende Funktionseinheit mit Code und Metadaten ist, besteht in C# keine Notwendigkeit für #include-Direktiven und Headerdateien. Die öffentlichen Typen und Member, die in einer bestimmten Assembly enthalten sind, werden einfach durch Verweisen auf die Assembly beim Kompilieren des Programms in einem C#-Programm verfügbar gemacht. Dieses Programm verwendet z.B. die Acme.Collections.Stack-Klasse aus der acme.dll-Assembly:

class Example
{
    public static void Main()
    {
        var s = new Acme.Collections.Stack<int>();
        s.Push(1); // stack contains 1
        s.Push(10); // stack contains 1, 10
        s.Push(100); // stack contains 1, 10, 100
        Console.WriteLine(s.Pop()); // stack contains 1, 10
        Console.WriteLine(s.Pop()); // stack contains 1
        Console.WriteLine(s.Pop()); // stack is empty
    }
}

Um dieses Programm zu kompilieren, müssen Sie auf die Assembly verweisen, die die im vorherigen Beispiel definierte Stapelklasse enthält.

C#-Programme können in mehreren Quelldateien gespeichert werden. Bei der Kompilierung eines C#-Programms werden alle Quelldateien zusammen verarbeitet. Die Quelldateien können frei aufeinander verweisen. Konzeptionell ist das Ganze so, als ob alle Quelldateien vor der Verarbeitung zu einer großen Datei verkettet worden wären. Vorwärtsdeklarationen sind in C# nie erforderlich, da die Reihenfolge der Deklaration mit wenigen Ausnahmen unbedeutend ist. C# beschränkt eine Quelldatei weder auf die Deklaration eines einzigen öffentlichen Typs, noch muss der Name der Quelldatei mit einem in der Quelldatei deklarierten Typ übereinstimmen.

In weiteren Artikeln in dieser Einführung werden diese Organisationsblöcke erläutert.