Freigeben über


Das C#-Typsystem

C# ist eine stark typierte Sprache. Jede Variable und Konstante weist einen Typ auf, ebenso wie jeder Ausdruck, der zu einem Wert ausgewertet wird. Jede Methodendeklaration gibt einen Namen, den Typ und die Art (Wert, Bezug oder Ausgabe) für jeden Eingabeparameter und für den Rückgabewert an. Die .NET-Klassenbibliothek definiert integrierte numerische Typen und komplexe Typen, die eine Vielzahl von Konstrukten darstellen. Dazu gehören das Dateisystem, Netzwerkverbindungen, Auflistungen und Arrays von Objekten und Datumsangaben. Ein typisches C#-Programm verwendet Typen aus der Klassenbibliothek und benutzerdefinierte Typen, die die Konzepte modellieren, die für die Problemdomäne des Programms spezifisch sind.

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

  • Der Speicherplatz, den eine Variable des Typs benötigt.
  • Die maximalen und minimalen Werte, die sie darstellen können.
  • Die Mitglieder (Methoden, Felder, Ereignisse usw.), die es enthält.
  • Der Basistyp, von dem es erbt.
  • Die von ihr implementierten Schnittstellen.
  • Die zulässigen Vorgänge.

Der Compiler verwendet Typinformationen, um sicherzustellen, dass alle Vorgänge, die in Ihrem Code ausgeführt werden, typsicher sind. Wenn Sie z. B. eine Variable vom Typ intdeklarieren, ermöglicht der Compiler die Verwendung der Variablen für Addition und Subtraktion. Wenn Sie versuchen, dieselben Vorgänge für eine Variable vom Typ boolauszuführen, generiert der Compiler einen Fehler, wie im folgenden Beispiel gezeigt:

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, beachten Sie, dass bool in C# nicht in int konvertierbar ist.

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 weiter zu gewährleisten, wenn sie Arbeitsspeicher zuweist und wieder zurückgibt.

Angeben von Typen in Variablendeklarationen

Wenn Sie eine Variable oder Konstante in einem Programm deklarieren, müssen Sie entweder den Typ angeben oder das var Schlüsselwort verwenden, damit der Compiler den Typ ableiten kann. Das folgende Beispiel zeigt einige Variablendeklarationen, 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 Typen von Methodenparametern und Rückgabewerten werden in der Methodendeklaration angegeben. Die folgende Signatur zeigt eine Methode, die ein int Eingabeargument erfordert und 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 mit einem neuen Typ neu deklarieren, und Sie können keinen Wert zuweisen, der nicht mit dem deklarierten Typ kompatibel ist. Sie können z. B. keinen int deklarieren und ihm dann einen booleschen Wert true zuweisen. Werte können jedoch in andere Typen konvertiert werden, z. B. wenn sie 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.

Eingebaute 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 Typen string und object. Diese Typen stehen Ihnen zur Verfügung, die Sie in einem beliebigen C#-Programm verwenden können. Die vollständige Liste der integrierten Typen finden Sie unter integrierten Typen.

Benutzerdefinierte Typen

Sie verwenden die struct, class, interface, enumund record Konstrukte, um eigene benutzerdefinierte Typen zu erstellen. Die .NET-Klassenbibliothek selbst ist eine Sammlung von benutzerdefinierten Typen, die Sie in Ihren eigenen Anwendungen verwenden können. Standardmäßig sind die am häufigsten verwendeten Typen in der Klassenbibliothek in einem beliebigen C#-Programm verfügbar. Andere werden nur verfügbar, wenn Sie der Assembly explizit einen Projektverweis hinzufügen, der sie definiert. Nachdem der Compiler über einen Verweis auf die Assembly verfügt, können Sie Variablen (und Konstanten) der typen deklarieren, die in dieser Assembly im Quellcode deklariert sind. Weitere Informationen finden Sie in der .NET-Klassenbibliothek.

Eine der ersten Entscheidungen, die Sie beim Definieren eines Typs treffen, ist die Entscheidung, welches Konstrukt für Ihren Typ verwendet werden soll. Die folgende Liste hilft ihnen, diese anfängliche Entscheidung zu treffen. Es gibt Überlappungen in den Auswahlmöglichkeiten. In den meisten Szenarien ist mehr als eine Option eine vernünftige Wahl.

  • Wenn die Größe des Datenspeichers klein ist, nicht mehr als 64 Byte, wählen Sie entweder struct oder record struct.
  • Wenn der Typ unveränderlich ist oder Sie eine nicht destruktive Mutation wünschen, wählen Sie ein struct oder record struct.
  • Wenn Ihr Typ Wertsemantik für Gleichheit aufweisen soll, wählen Sie ein record class oder ein record struct.
  • Wenn der Typ in erster Linie zum Speichern von Daten verwendet wird, nicht zum Verhalten, wählen Sie ein record class oder aus record struct.
  • Wenn der Typ Teil einer Vererbungshierarchie ist, wählen Sie ein record class oder ein class.
  • Wenn der Typ Polymorphismus verwendet, wählen Sie eine class.
  • Wenn der primäre Zweck das Verhalten ist, wählen Sie eine class.

Das einheitliche Typsystem

Es ist wichtig, zwei grundlegende Punkte über das Typsystem in .NET zu verstehen:

  • Es unterstützt das Vererbungsprinzip. Typen können von anderen Typen abgeleitet werden, die als Basistypen bezeichnet werden. Der abgeleitete Typ erbt (mit einigen Einschränkungen) die Methoden, Eigenschaften und andere Member des Basistyps. Der Basistyp kann wiederum von einem anderen Typ abgeleitet werden. In diesem Fall erbt der abgeleitete Typ die Member beider Basistypen in seiner Vererbungshierarchie. Alle Typen, einschließlich integrierter numerischer Typen wie System.Int32 (C#-Schlüsselwort: int), leiten letztendlich von einem einzelnen Basistyp ab, der ( System.Object C#-Schlüsselwort: object). Diese einheitliche Typhierarchie wird als Common Type System (CTS) bezeichnet. Weitere Informationen zur Vererbung in C# finden Sie unter "Vererbung".
  • Jeder Typ im CTS wird entweder als Werttyp oder als Bezugstyp definiert. Zu diesen Typen gehören alle benutzerdefinierten Typen in der .NET-Klassenbibliothek und auch Ihre eigenen benutzerdefinierten Typen. Typen, die Sie mithilfe des struct Schlüsselworts definieren, sind Werttypen; alle integrierten numerischen Typen sind structs. Typen, die Sie mit dem Schlüsselwort class oder record definieren, sind Referenztypen. Referenztypen und Werttypen weisen unterschiedliche Kompilierungszeitregeln und unterschiedliche Laufzeitverhalten auf.

Die folgende Abbildung zeigt die Beziehung zwischen Werttypen und Referenztypen im CTS.

Screenshot mit CTS-Werttypen und Referenztypen.

Hinweis

Sie können sehen, dass die am häufigsten verwendeten Typen im System Namespace organisiert sind. Der Namespace, in dem ein Typ enthalten ist, hat jedoch keine Beziehung dazu, ob es sich um einen Werttyp oder einen Verweistyp handelt.

Klassen und Strukturen sind zwei der grundlegenden Konstrukte des allgemeinen Typsystems in .NET. Jede ist im Wesentlichen eine Datenstruktur, die eine Gruppe von Daten und Verhaltensweisen kapselt, die als logische Einheit zusammen gehören. Die Daten und Verhaltensweisen sind die Mitglieder der Klasse, Struktur oder Datensatz. Die Member beinhalten ihre Methoden, Eigenschaften, Ereignisse usw. und sind weiter unten in diesem Artikel aufgeführt.

Eine Klasse, Struktur oder Datensatzdeklaration ist wie ein Blueprint, der zum Erstellen von Instanzen oder Objekten zur Laufzeit verwendet wird. Wenn Sie eine Klasse, Struktur oder einen Datensatz mit dem Namen Persondefinieren, Person ist der Name des Typs. Wenn Sie eine Variable p vom Typ Persondeklarieren und initialisieren, wird p als Objekt oder Instanz von Personbezeichnet. Es können mehrere Instanzen desselben Person Typs erstellt werden, und jede Instanz kann unterschiedliche Werte in ihren Eigenschaften und Feldern aufweisen.

Eine Klasse ist ein Verweistyp. Wenn ein Objekt des Typs erstellt wird, enthält die Variable, der das Objekt zugewiesen ist, nur einen Verweis auf diesen Speicher. Wenn der Objektverweis einer neuen Variablen zugewiesen ist, verweist die neue Variable auf das ursprüngliche Objekt. Änderungen, die an einer Variablen vorgenommen werden, spiegeln sich in der anderen Variablen wider, da beide auf dieselben Daten verweisen.

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

Datensatztypen können entweder Bezugstypen (record class) oder Werttypen (record struct) sein. Datensatztypen enthalten Methoden, die die Wertgleichstellung unterstützen.

Im Allgemeinen werden Klassen verwendet, um komplexeres Verhalten zu modellieren. Klassen speichern in der Regel Daten, die nach dem Erstellen eines Klassenobjekts geändert werden sollen. Strukturen eignen sich am besten für kleine Datenstrukturen. Strukturen speichern in der Regel Daten, die nicht geändert werden sollen, nachdem die Struktur erstellt wurde. Datensatztypen sind Datenstrukturen mit zusätzlichen kompilergenerierten Mitgliedern. Datensätze speichern in der Regel Daten, die nicht geändert werden sollen, nachdem das Objekt erstellt wurde.

Werttypen

Werttypen werden von System.ValueTypeabgeleitet, der von System.Objectabgeleitet wird. Typen, die von System.ValueType abgeleitet sind, haben ein spezielles Verhalten in der CLR. Werttypvariablen enthalten ihre Werte direkt. Der Speicher für eine Struktur wird inline in dem Kontext zugewiesen, in dem die Variable deklariert wird. Es gibt keinen separaten Heap-Zuweisungs- oder Garbage Collection-Aufwand für Werttypvariablen. Sie können record struct Typen deklarieren, die Werttypen sind und die synthetisierten Member für Datensätze einschließen.

Es gibt zwei Kategorien von Werttypen: struct und enum.

Die integrierten numerischen Typen sind Strukturen und weisen Felder und Methoden auf, auf die Sie zugreifen können:

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

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

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

Werttypen sind versiegelt. Sie können einen Typ nicht von einem Werttyp ableiten, z. B. System.Int32. Sie können keine Struktur definieren, die von einer benutzerdefinierten Klasse oder Struktur erbt, da eine Struktur nur von System.ValueType erben kann. Eine Struktur kann jedoch eine oder mehrere Schnittstellen implementieren. Sie können einen Strukturtyp in jeden von ihr implementierten Schnittstellentyp umwandeln. 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 verwenden das Strukturschlüsselwort , um eigene benutzerdefinierte Werttypen zu erstellen. In der Regel wird eine Struktur als Container für eine kleine Gruppe verwandter Variablen verwendet, wie im folgenden Beispiel gezeigt:

public struct Coords
{
    public int x, y;

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

Weitere Informationen zu Strukturtypen finden Sie unter "Strukturtypen". Weitere Informationen zu Werttypen finden Sie unter Werttypen.

Die andere Kategorie von Werttypen ist enum. Eine Enum definiert eine Reihe benannter Ganzzahlkonstanten. Beispielsweise enthält die System.IO.FileMode-Aufzählung in der .NET-Klassenbibliothek einen Satz benannter Konstantenzahlen, die angeben, wie eine Datei geöffnet werden soll. Es ist wie im folgenden Beispiel dargestellt definiert:

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

Die System.IO.FileMode.Create Konstante weist den Wert 2 auf. Der Name ist jedoch viel aussagekräftiger für Menschen, die den Quellcode lesen, und aus diesem Grund ist es besser, Enumerationen anstelle konstanter Literalzahlen 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.

Referenztypen

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

Wenn Sie eine Variable eines Typs reference typedeklarieren, enthält sie den Wert null , bis Sie sie einer Instanz dieses Typs zuweisen oder eine mit dem new Operator erstellen. Die Erstellung und Zuweisung einer Klasse wird im folgenden Beispiel veranschaulicht:

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

Ein interface kann nicht direkt mit dem Operator new instanziiert werden. Erstellen und zuweisen Sie stattdessen eine Instanz einer Klasse, die die Schnittstelle implementiert. 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 die Position 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 Rückgewinnung durchführt. Die Garbage Collection ist jedoch auch stark optimiert. In den meisten Szenarien führt sie nicht zu einem Leistungsproblem. Weitere Informationen zur Müllabfuhr, siehe Automatische Speicherverwaltung.

Alle Arrays sind Referenztypen, auch wenn ihre Elemente Werttypen sind. Arrays leiten implizit von der System.Array Klasse ab. Sie deklarieren und verwenden sie mit der vereinfachten Syntax, die von C# bereitgestellt wird, wie im folgenden Beispiel gezeigt:

// 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. Wenn Sie eine Klasse erstellen, können Sie von einer 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 zu Vererbung und virtuellen Methoden finden Sie unter Vererbung.

Typen von Literalwerten

In C# erhalten Literalwerte einen Typ vom Compiler. Sie können angeben, wie ein numerisches Literal eingegeben werden soll, indem Sie am Ende der Zahl einen Buchstaben anfügen. Um beispielsweise anzugeben, dass der Wert 4.56 als float behandelt werden soll, hängen Sie ein "f" oder "F" an die Zahl an: 4.56f. Wenn kein Buchstabe angefügt wird, leitet der Compiler einen Typ für das Literal ab. Weitere Informationen dazu, welche Typen mit Buchstabensuffixen angegeben werden können, finden Sie unter Integrale numerische Typen und 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 tatsächlichen Typ (den konkreten Typ) dienen. Clientcode stellt den konkreten Typ bereit, wenn er eine Instanz des Typs erstellt. Solche Typen werden als generische Typen bezeichnet. Der .NET-Typ System.Collections.Generic.List<T> verfügt beispielsweise über einen Typparameter, der standardmäßig den Namen Terhält. Wenn Sie eine Instanz des Typs erstellen, geben Sie den Typ der Objekte an, die die Liste enthält, 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 es, dieselbe Klasse wiederzuverwenden, um einen beliebigen Elementtyp zu enthalten, ohne jedes Element in ein Objekt konvertieren zu müssen. Generische Auflistungsklassen werden als stark typierte Auflistungen bezeichnet, da der Compiler den spezifischen Typ der Elemente der Auflistung kennt und einen Fehler beim Kompilieren 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 Nullwerte

Sie können eine lokale Variable (aber keine Klassenmitglieder) implizit deklarieren, indem Sie das var Schlüsselwort verwenden. Die Variable empfängt zur Kompilierungszeit immer noch einen Typ, aber der Typ wird vom Compiler bereitgestellt. Weitere Informationen finden Sie unter Implizit typierten lokalen Variablen.

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

Normale Werttypen können keinen Wert von null haben. Sie können jedoch Nullwertetypen erstellen, indem Sie einen ? nach dem Typ anfügen. Beispielsweise ist int? ein int Typ, der auch den Wert null haben kann. Nullable Wertetypen sind Instanzen des generischen Strukturtyps System.Nullable<T>. Nullable-Werttypen sind besonders nützlich, wenn Sie Daten an und aus Datenbanken übergeben, in denen numerische Werte auftreten können null. Weitere Informationen finden Sie unter Nullwertetypen.

Kompilierungszeittyp und Laufzeittyp

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

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

In anderen Fällen unterscheidet sich der Kompilierungszeittyp, wie in den folgenden beiden Beispielen gezeigt:

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

In beiden vorherigen Beispielen ist der Laufzeittyp ein string. Der Kompilierungszeittyp befindet sich object in der ersten Zeile und IEnumerable<char> im zweiten.

Wenn die beiden Typen für eine Variable unterschiedlich sind, ist es wichtig zu verstehen, wann der Kompilierungszeittyp und der Laufzeittyp angewendet werden. Der Kompilierungszeittyp bestimmt alle Vom Compiler ausgeführten Aktionen. Diese Compileraktionen umfassen die Auflösung von Methodenaufrufen, die Überladungsauflösung und verfügbare implizite und explizite Umwandlungen. Der Laufzeittyp bestimmt alle Aktionen, die zur Laufzeit aufgelöst werden. Diese Laufzeitaktionen umfassen das Versenden virtueller Methodenaufrufe, das Auswerten von is und switch Ausdrücken sowie andere Typüberprüfungs-APIs. Um besser zu verstehen, wie Ihr Code mit Typen interagiert, erkennen 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 endgültige Quelle für C#-Syntax und -Verwendung.