Das C#-Typsystem

C# ist eine stark typisierte Sprache. Jede Variable und jede Konstante verfügt über einen Typ, genau wie jeder Ausdruck, der zu einem Wert ausgewertet wird. Jede Methodendeklaration gibt den Namen, den Typ und die Art (Wert, Verweis oder Ausgabe) für jeden Eingabeparameter und Rückgabewert an. In der .NET-Klassenbibliothek sind integrierte numerische Typen und komplexe Typen definiert, die für eine große Bandbreite an Konstrukten stehen. Dazu gehören das Dateisystem, Netzwerkverbindungen, Sammlungen und Arrays von Objekten sowie Datumsangaben. In einem typischen C#-Programm werden Typen aus der Klassenbibliothek und benutzerdefinierte Typen verwendet, die die Konzepte für das Problemfeld des Programms modellieren.

Die in einem Typ gespeicherten Informationen können die folgenden Elemente umfassen:

  • Der Speicherplatz, den eine Variable des Typs erfordert
  • Die maximalen und minimalen Werte, die diese darstellen kann
  • Die enthaltenen Member (Methoden, Felder, Ereignisse usw.)
  • Der Basistyp, von dem geerbt wird
  • Die Schnittstellen, die implementiert werden
  • Die Arten von zulässigen Vorgängen

Der Compiler verwendet Typinformationen, um sicherzustellen, dass alle im Code ausgeführten Vorgänge typsicher sind. Wenn Sie z. B. eine Variable vom Typ int deklarieren, können Sie mit dem Compiler die Variable für Additions- und Subtraktionsvorgänge verwenden. Wenn Sie dieselben Vorgänge für eine Variable vom Typ bool ausführen möchten, generiert der Compiler einen Fehler, wie im folgenden Beispiel dargestellt:

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

Hinweis

C- und C++-Entwickler sollten beachten, dass in C# bool nicht in int konvertiert werden kann.

Der Compiler bettet die Typinformationen als Metadaten in die ausführbare Datei ein. Die Common Language Runtime (CLR) verwendet diese Metadaten zur Laufzeit, um die Typsicherheit zu gewährleisten, wenn Speicherplatz belegt und freigegeben wird.

Angeben von Typen in Variablendeklarationen

Wenn Sie eine Variable oder Konstante in einem Programm deklarieren, müssen Sie ihren Typ festlegen oder das var-Schlüsselwort verwenden, damit der Typ vom Compiler abgeleitet wird. Im folgenden Beispiel werden einige Variablendeklarationen dargestellt, die sowohl integrierte numerische Typen als auch komplexe benutzerdefinierte Typen verwenden:

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
            where item <= limit
            select item;

Die Methodenparameter- und Rückgabewerttypen werden in der Methodendeklaration angegeben. Die folgende Signatur zeigt eine Methode, für die ein int als Eingabeargument benötigt wird und die eine Zeichenfolge zurückgibt:

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };

Nachdem Sie eine Variable deklariert haben, können Sie sie nicht erneut mit einem neuen Typ deklarieren, und Sie können keinen Wert zuweisen, der nicht mit ihrem deklarierten Typ kompatibel ist. Beispielsweise können Sie keinen Typ int deklarieren und diesem dann den booleschen Wert true zuweisen. Werte können jedoch in andere Typen konvertiert werden, etwa wenn diese neuen Variablen zugewiesen oder als Methodenargumente übergeben werden. Eine Typkonvertierung, die keinen Datenverlust verursacht, wird automatisch vom Compiler ausgeführt. Eine Konvertierung, die möglicherweise Datenverlust verursacht, erfordert eine Umwandlung in den Quellcode.

Weitere Informationen finden Sie unter Umwandlung und Typkonvertierungen.

Integrierte Typen

C# stellt einen Standardsatz integrierter Typen bereit. Diese stellen ganze Zahlen, Gleitkommawerte, boolesche Ausdrücke, Textzeichen, Dezimalwerte und andere Datentypen dar. Es gibt auch integrierte string-Typen und object-Typen. Diese Typen können Sie in jedem C#-Programm verwenden. Eine vollständige Liste der integrierten Typen finden Sie unter Integrierte Typen.

Benutzerdefinierte Typen

Sie verwenden die Konstrukte struct, class, interface, enum und record zum Erstellen Ihrer eigenen benutzerdefinierten Typen. Die .NET-Klassenbibliothek ist eine Sammlung benutzerdefinierter Typen, die Sie in Ihren eigenen Anwendungen verwenden können. Standardmäßig sind die am häufigsten verwendeten Typen in der Klassenbibliothek in jedem C#-Programm verfügbar. Andere stehen nur zur Verfügung, wenn Sie ausdrücklich einen Projektverweis auf die Assembly hinzufügen, in der sie definiert sind. Wenn der Compiler über einen Verweis auf die Assembly verfügt, können Sie Variablen (und Konstanten) des in dieser Assembly deklarierten Typs im Quellcode deklarieren. Weitere Informationen finden Sie in der Dokumentation zur .NET-Klassenbibliothek.

Das allgemeine Typsystem

Es ist wichtig, zwei grundlegende Punkte zum Typsystem in .NET zu verstehen:

  • Es unterstützt das Prinzip der Vererbung. Typen können von anderen Typen abgeleitet werden, die als Basistypen bezeichnet werden. Der abgeleitete Typ erbt (mit einigen Beschränkungen) die Methoden, Eigenschaften und anderen Member des Basistyps. Der Basistyp kann wiederum von einem anderen Typ abgeleitet sein. In diesem Fall erbt der abgeleitete Typ die Member beider Basistypen in der Vererbungshierarchie. Alle Typen, einschließlich integrierter numerischer Typen, z.B. System.Int32 (C#-Schlüsselwort: int), werden letztendlich von einem einzelnen Basistyp abgeleitet, nämlich System.Object (C#-Schlüsselwort: object). Diese einheitliche Typhierarchie wird als Allgemeines Typsystem (CTS) bezeichnet. Weitere Informationen zur Vererbung in C# finden Sie unter Vererbung.
  • Jeder Typ im CTS ist als Werttyp oder Referenztyp definiert. Diese Typen umfassen auch alle benutzerdefinierten Typen in der .NET-Klassenbibliothek und Ihre eigenen benutzerdefinierten Typen. Typen, die Sie mithilfe des struct-Schlüsselworts definieren, sind Werttypen. Alle integrierten numerischen Typen sind structs. Typen, die Sie mithilfe des class- oder record-Schlüsselworts definieren, sind Referenztypen. Für Referenztypen und Werttypen gelten unterschiedliche Kompilierzeitregeln und ein anderes Laufzeitverhalten.

In der folgenden Abbildung wird die Beziehung zwischen Werttypen und Referenztypen im CTS dargestellt.

Screenshot mit CTS-Werttypen und -Verweistypen.

Hinweis

Wie Sie sehen, sind die am häufigsten verwendeten Typen alle im System-Namespace organisiert. Jedoch ist es für den Namespace, in dem ein Typ enthalten ist, unerheblich, ob es sich um einen Werttyp oder einen Referenztyp handelt.

Klassen und Strukturen sind zwei der grundlegenden Konstrukte des allgemeinen Typsystems in .NET. C# 9 fügt Datensätze hinzu, bei denen es sich um eine Art von Klasse handelt. Bei beiden handelt es sich um eine Datenstruktur, die einen als logische Einheit zusammengehörenden Satz von Daten und Verhalten kapselt. Die Daten und Verhalten bilden die Member der Klasse, der Struktur oder des Datensatzes. Die Member beinhalten ihre Methoden, Eigenschaften, Ereignisse usw. und sind weiter unten in diesem Artikel aufgeführt.

Die Deklaration einer Klasse, Struktur oder eines Datensatzes ist mit einer Blaupause vergleichbar, mit der zur Laufzeit Instanzen oder Objekte erstellt werden. Wenn Sie eine Klasse, Struktur oder einen Datensatz namens Person definieren, ist Person der Name des Typs. Wenn Sie eine Variable p vom Typ Person deklarieren und initialisieren, wird p als Objekt oder Instanz von Person bezeichnet. Vom selben Typ Person können mehrere Instanzen erstellt werden, und jede Instanz kann über unterschiedliche Werte in ihren Eigenschaften und Feldern verfügen.

Eine Klasse ist ein Verweistyp. Wenn ein Objekt des Typs erstellt wird, enthält die Variable, der das Objekt zugewiesen wurde, lediglich einen Verweis auf den entsprechenden Speicherort. Wenn der Objektverweis einer neuen Variablen zugewiesen wird, verweist die neue Variable auf das ursprüngliche Objekt. Über eine Variable vorgenommene Änderungen gelten auch für die andere Variable, da beide auf dieselben Daten verweisen.

Eine Struktur ist ein Werttyp. Wenn eine Struktur erstellt wird, enthält die Variable, der die Struktur zugewiesen wird, die eigentlichen Daten der Struktur. Wenn die Struktur einer neuen Variable zugewiesen wird, wird sie kopiert. Die neue Variable und die ursprüngliche Variable enthalten daher zwei separate Kopien der gleichen Daten. Änderungen an einer Kopie wirken sich nicht auf die andere Kopie aus.

Datensatztypen können entweder Verweistypen (record class) oder Werttypen (record struct) sein.

Im Allgemeinen werden Klassen verwendet, um komplexeres Verhalten zu modellieren. Klassen speichern in der Regel Daten, die geändert werden sollen, nachdem ein Klassenobjekt erstellt wurde. Strukturen eignen sich am besten für kleine Datenstrukturen. In Strukturen sind in der Regel Daten gespeichert, deren Änderung nach dem Erstellen der Struktur nicht beabsichtigt ist. Datensatztypen sind Datenstrukturen mit zusätzlichen vom Compiler synthetisierten Membern. In Datensätzen sind in der Regel Daten gespeichert, deren Änderung nach dem Erstellen des Objekts nicht beabsichtigt ist.

Werttypen

Werttypen werden von System.ValueType abgeleitet, was wiederum von System.Object abgeleitet wird. Typen, die von System.ValueType abgeleitet werden, weisen ein besonderes Verhalten in der CLR auf. Werttypvariablen enthalten ihre Werte direkt. Der Arbeitsspeicher für eine Struktur wird inline in dem Kontext zugeordnet, in dem die Variable deklariert ist. Für Werttypvariablen erfolgt keine getrennte Heapzuordnung bzw. kein Mehraufwand für Garbage Collection. Sie können record struct-Typen deklarieren, die Werttypen sind, und die synthetisierten Member für Datensätze einschließen.

Zwei Kategorien von Werttypen sind verfügbar: struct und enum.

Die integrierten numerischen Typen sind Strukturen und verfügen über Felder und Methoden, auf die Sie zugreifen können:

// constant field on type byte.
byte b = byte.MaxValue;

Sie deklarieren diese jedoch und weisen ihnen Werte zu, als wären es einfache, nicht aggregierte Typen:

byte num = 0xA;
int i = 5;
char c = 'Z';

Werttypen sind versiegelt. Sie können keinen Typ aus einem Werttyp ableiten, z. B. System.Int32. Sie können keine Struktur definieren, die von einer benutzerdefinierten Klasse oder Struktur erben kann, weil eine Struktur nur von System.ValueType erben kann. Eine Struktur kann jedoch eine oder mehrere Schnittstellen implementieren. Sie können einen Strukturtyp in jeden beliebigen Schnittstellentyp umwandeln, den er implementiert. Diese Umwandlung verursacht einen Boxing-Vorgang, mit dem die Struktur von einem Referenztypobjekt im verwalteten Heap umschlossen wird. Boxing-Vorgänge werden auch ausgeführt, wenn Sie einen Werttyp an eine Methode übergeben, die System.Object oder einen beliebigen Schnittstellentyp als Eingabeparameter akzeptiert. Weitere Informationen finden Sie unter Boxing und Unboxing.

Sie können das struct-Schlüsselwort verwenden, um eigene benutzerdefinierte Werttypen zu erstellen. In der Regel wird eine Struktur als Container für einen kleinen Satz verwandter Variablen verwendet, wie im folgenden Beispiel dargestellt:

public struct Coords
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

Weitere Informationen über Strukturen finden Sie unter Struktur-Typen. Weitere Informationen zu Werttypen finden Sie unter Werttypen.

Die andere Kategorie von Werttypen ist enum. Eine Enumeration definiert einen Satz benannter ganzzahliger Konstanten. So enthält z.B. die System.IO.FileMode-Enumeration in der .NET-Klassenbibliothek mehrere benannte ganzzahlige Konstanten, die festlegen, wie eine Datei geöffnet werden soll. Die Definition erfolgt wie im folgenden Beispiel:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

Die System.IO.FileMode.Create-Konstante besitzt den Wert 2. Der Name ist jedoch für Personen, die den Quellcode lesen, viel aussagekräftiger. Aus diesem Grund ist es besser, anstelle von Konstantenliteralen Enumerationen zu verwenden. Weitere Informationen finden Sie unter System.IO.FileMode.

Alle Enumerationen erben von System.Enum, was wiederum von System.ValueType erbt. Alle Regeln, die für Strukturen gelten, gelten auch für Enumerationen. Weitere Informationen zu Enumerationen finden Sie unter Enumerationstypen.

Verweistypen

Ein Typ, der als class, record, delegate, Array oder interface definiert ist, ist ein reference type.

Wenn Sie eine Variable eines reference type deklarieren, enthält sie den Wert null, bis Sie ihr eine Instanz dieses Typs zuweisen oder über den new-Operator eine Instanz erstellen. Das folgende Beispiel veranschaulicht die Erstellung und Zuweisung einer Klasse:

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

interface kann nicht direkt über den new-Operator instanziiert werden. Erstellen Sie stattdessen eine Instanz einer Klasse, die die Schnittstelle implementiert, und weisen Sie sie zu. Betrachten Sie das folgenden Beispiel:

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

Beim Erstellen des Objekts wird der Arbeitsspeicher auf dem verwalteten Heap zugewiesen. Die Variable enthält nur einen Verweis auf den Speicherort des Objekts. Für Typen im verwalteten Heap ist sowohl bei der Zuweisung als auch bei der Bereinigung Mehraufwand erforderlich. Garbage Collection ist die automatische Speicherverwaltungsfunktion der CLR, die die Bereinigung ausführt. Die Garbage Collection ist jedoch auch stark optimiert. In den meisten Szenarien führt sie nicht zu einem Leistungsproblem. Weitere Informationen zur Garbage Collection finden Sie unter Automatische Speicherverwaltung.

Alle Arrays sind Referenztypen, selbst wenn ihre Elemente Werttypen sind. Arrays werden implizit von der System.Array-Klasse abgeleitet. Sie deklarieren und verwenden diese jedoch mit der vereinfachten, von C# bereitgestellten Syntax, wie im folgenden Beispiel dargestellt:

// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.
int len = nums.Length;

Referenztypen bieten volle Vererbungsunterstützung. Beim Erstellen einer Klasse können Sie von jeder anderen Schnittstelle oder Klasse erben, die nicht als versiegelt definiert ist. Andere Klassen können von Ihrer Klasse erben und Ihre virtuellen Methoden überschreiben. Weitere Informationen zum Erstellen eigener Klassen finden Sie unter Klassen, Strukturen und Datensätze. Weitere Informationen zur Vererbung und zu virtuellen Methoden finden Sie unter Vererbung.

Typen von Literalwerten

In C# erhalten Literalwerte einen Typ vom Compiler. Sie können festlegen, wie ein numerisches Literal eingegeben werden soll, indem Sie am Ende der Zahl einen Buchstaben anfügen. Um z. B. anzugeben, dass der Wert 4.56 als float behandelt werden soll, fügen Sie nach der Zahl 4.56f ein "f" oder "F" an. Wenn kein Buchstabe angefügt wird, leitet der Compiler einen Typ für das Literal ab. Weitere Informationen darüber, welche Typen mit Buchstabensuffixen angegeben werden können, finden Sie unter Integrale numerische Typen und unter Numerische Gleitkommatypen.

Da Literale typisiert sind und alle Typen letztlich von System.Object abgeleitet werden, können Sie Code der folgenden Art erstellen und kompilieren:

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

Generische Typen

Ein Typ kann mit einem oder mehreren Typparametern deklariert werden, die als Platzhalter für den eigentlichen Typ verwendet werden (den konkreten Typ). Der konkrete Typ wird beim Erstellen einer Instanz des Typs vom Clientcode bereitgestellt. Solche Typen werden als generische Typen bezeichnet. Beispielsweise besitzt der .NET-Typ System.Collections.Generic.List<T> einen Typparameter, der konventionsgemäß den Namen T erhält. Beim Erstellen einer Instanz des Typs geben Sie den Typ der Objekte an, die die Liste enthalten soll, z. B. string:

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

Die Verwendung des Typparameters ermöglicht die Wiederverwendung der Klasse für beliebige Elementtypen, ohne die einzelnen Elemente in object konvertieren zu müssen. Generische Sammlungsklassen werden als stark typisierte Sammlungen bezeichnet, weil der Compiler den jeweiligen Typ der Elemente in der Sammlung kennt und zur Kompilierzeit einen Fehler auslösen kann, wenn Sie beispielsweise versuchen, dem stringList-Objekt im vorherigen Beispiel eine ganze Zahl hinzuzufügen. Weitere Informationen finden Sie unter Generics.

Implizite Typen, anonyme Typen und Werttypen, die Nullwerte zulassen

Sie können eine lokale Variable (jedoch keine Klassenmember) implizit eingeben, indem Sie das var-Schlüsselwort verwenden. Die Variable erhält weiterhin zur Kompilierzeit einen Typ, aber der Typ wird vom Compiler bereitgestellt. Weitere Informationen zu finden Sie unter Implizit typisierte lokale Variablen.

Es kann unpraktisch sein, einen benannten Typ für einfache Sätze verwandter Werte zu erstellen, die nicht außerhalb von Methodengrenzen gespeichert oder übergeben werden sollen. Sie können für diesen Zweck anonyme Typen erstellen. Weitere Informationen finden Sie unter Anonyme Typen.

Gewöhnliche Werttypen können nicht den Wert null aufweisen. Sie können jedoch Nullwerte zulassende Werttypen erstellen, indem Sie nach dem Typ ein ? anfügen. Zum Beispiel ist int? ein int-Typ, der auch den Wert null aufweisen kann. Werttypen, die Nullwerte zulassen, sind Instanzen vom generischen Strukturtyp System.Nullable<T>. Werttypen, die Nullwerte zulassen, sind besonders hilfreich, wenn Sie Daten an und aus Datenbanken übergeben, in denen die numerischen Werte null sein können. Weitere Informationen finden Sie unter Werttypen, die Nullwerte zulassen.

Typ zur Kompilierzeit und Typ zur Laufzeit

Eine Variable kann unterschiedliche Kompilierzeit- und Laufzeittypen aufweisen. Der Typ zur Kompilierzeit ist der deklarierte oder abgeleitete Typ der Variablen im Quellcode. Der Typ zur Laufzeit ist der Typ der Instanz, auf die von dieser Variablen verwiesen wird. Häufig sind diese beiden Typen identisch, wie im folgenden Beispiel gezeigt:

string message = "This is a string of characters";

In anderen Fällen ist der Typ zur Kompilierzeit ein anderer, wie in den folgenden beiden Beispielen gezeigt:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

In den beiden vorherigen Beispielen ist der Typ zur Laufzeit ein Typ string. Der Typ zur Kompilierzeit ist object in der ersten Zeile und IEnumerable<char> in der zweiten Zeile.

Wenn sich die beiden Typen für eine Variable unterscheiden, ist es wichtig zu verstehen, wann der Typ zur Kompilierzeit und wann der Typ zur Laufzeit auftritt. Der Typ zur Kompilierzeit bestimmt alle Aktionen, die vom Compiler ausgeführt werden. Diese Compileraktionen umfassen die Auflösung von Methodenaufrufen, die Überladungsauflösung und verfügbare implizite und explizite Umwandlungen. Der Typ zur Laufzeit bestimmt alle Aktionen, die zur Laufzeit aufgelöst werden. Diese Laufzeitaktionen umfassen das Verteilen virtueller Methodenaufrufe, das Auswerten von is- und switch-Ausdrücken sowie andere Typtest-APIs. Um besser zu verstehen, wie der Code mit Typen interagiert, ermitteln Sie, welche Aktion für welchen Typ gilt.

Weitere Informationen finden Sie in den folgenden Artikeln:

C#-Sprachspezifikation

Weitere Informationen erhalten Sie unter C#-Sprachspezifikation. Die Sprachspezifikation ist die verbindliche Quelle für die Syntax und Verwendung von C#.