Untersuchen des .NET-Typsystems
C# ist eine stark typierte Sprache. Jede Variable und Konstante weist einen Typ auf, ebenso wie jeder Ausdruck, der zu einem Wert ausgewertet wird. Die .NET-Klassenbibliothek definiert integrierte numerische Typen und komplexe Typen, die eine Vielzahl von Konstrukten darstellen. Diese Konstrukte umfassen das Dateisystem, Netzwerkverbindungen, Sammlungen 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.
Integrierte Typen
C# stellt einen Standardsatz integrierter Typen bereit. Diese Standardtypen stellen ganze Zahlen, Gleitkommawerte, boolesche Ausdrücke, Textzeichen, Dezimalwerte und andere Datentypen dar. Es gibt auch integrierte Zeichenfolgen- und Objekttypen. Diese Typen stehen Ihnen zur Verfügung, die Sie in einem beliebigen C#-Programm verwenden können.
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.
Allgemeines 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.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 Strukturschlüsselworts definieren, sind Werttypen; Alle integrierten numerischen Typen sind Strukturen. Typen, die Sie mithilfe des Schlüsselworts "Klasse" oder "Datensatz" 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.
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 Member der class, structoder record. Die Member einer Klasse umfassen Eigenschaften (Daten), Methoden (Verhalten), Felder (variablen, die innerhalb der Klasse deklariert wurden) und viele andere.
Eine class-, struct- oder record-Deklaration ist wie eine Blaupause, die zum Erstellen von Instanzen oder Objekten zur Laufzeit verwendet wird. Wenn Sie eine Klasse mit dem Namen Persondefinieren, ist Person 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 über eine Variable vorgenommen wurden, werden in der anderen Variablen widerzuspiegeln, da beide auf dieselben Daten verweisen.
Ein struct ist ein Werttyp. Wenn ein struct erstellt wird, enthält die Variable, der die struct zugewiesen wird, die tatsächlichen Daten des struct. Wenn der struct einer neuen Variablen zugewiesen wird, wird der Datenwert 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 struct erstellt wurde. Datensatztypen sind Datenstrukturen mit anderen compilersynthetisierten Membern. Datensätze speichern in der Regel Daten, die nicht geändert werden sollen, nachdem das Objekt erstellt wurde.
Werttypen
Werttypen werden von System.ValueTypeabgeleitet, die von System.Objectabgeleitet werden. Typen, die von System.ValueType abgeleitet werden, weisen ein spezielles Verhalten in der Common Language Runtime auf. Werttypvariablen enthalten ihre Werte direkt. Der Speicher für eine struct 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 Datensatzstrukturtypen deklarieren, die Werttypen sind, und die synthetisierten Member für Datensätze einschließen.
Es gibt zwei Kategorien von Werttypen: Struktur und Enumeration.
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 Beliebigen Werttyp ableiten, z. B. System.Int32. Sie können keine struct definieren, die von benutzerdefinierten class oder struct erben sollen, da ein struct nur von System.ValueTypeerben kann.
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;
}
}
Die andere Kategorie von Werttypen ist enum. Ein enum definiert einen Satz benannter Integralkonstanten. 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. Die Syntax für eine enum wird im folgenden Beispiel gezeigt:
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.
Alle enums erben von System.Enum, die von System.ValueTypeerbt. Alle Regeln, die für structs gelten, gelten auch für enums.
Referenztypen
Die class, record, delegate, array, und interface Typen sind Referenztypen.
Wenn Sie eine Variable eines Bezugstyps deklarieren, enthält sie den Wert null, bis Sie sie einer Instanz dieses Typs zuweisen oder eine mithilfe des new-Operators erstellen.
Im folgenden Beispiel wird veranschaulicht, wie Verweistypvariablen mithilfe von Arrays deklariert werden:
// Declaring an array variable
int[] numbers;
// Initializing the array with a size of 5
numbers = new int[5];
// Alternatively, declaring and initializing an array in one line
int[] numbers2 = new int[] { 1, 2, 3, 4, 5 };
// Assigning a reference to another variable
int[] numbers3 = numbers2;
Der operator new erstellt eine Instanz des Typs und gibt einen Verweis auf diese Instanz zurück. Der Verweis ist die Speicheradresse des Objekts, und dieser Verweis wird in der Variablen gespeichert. Wenn Sie einer anderen Variablen eine Verweistypvariable zuweisen, kopieren Sie den Verweis, nicht das Objekt selbst. Beide Variablen verweisen auf dasselbe Objekt im Arbeitsspeicher.
Anmerkung
Arrays sind nicht nur Referenztypen, sie sind Auflistungen. Auflistungen können mithilfe von Auflistungsausdrücken initialisiert werden, wodurch die Anforderung ausgeschlossen wird, das schlüsselwort new beim Deklarieren und Initialisieren eines Arrays in einer Zeile einzuschließen. Beispiel: int[] numbers = [ 1, 2, 3, 4, 5 ];.
Sie können eine Instanz einer Klasse mithilfe derselben Syntax erstellen, die zum Instanziieren der integrierten Typen verwendet wird. Im folgenden Beispiel wird veranschaulicht, wie eine Instanz einer Klasse erstellt wird:
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
Der new-Operator erstellt eine Instanz der Klasse und gibt einen Verweis auf diese Instanz zurück. Der Verweis ist die Speicheradresse des Objekts, und dieser Verweis wird in der Variablen gespeichert. Wenn Sie einer anderen Variablen eine Verweistypvariable zuweisen, kopieren Sie den Verweis, nicht das Objekt selbst. Beide Variablen verweisen auf dasselbe Objekt im Arbeitsspeicher.