C#-Typen und -Member

Als objektorientierte Sprache unterstützt C# die Konzepte der Kapselung, Vererbung und Polymorphie. Eine Klasse kann direkt von einer übergeordneten Klasse erben und eine beliebige Anzahl von Schnittstellen implementieren. Methoden, die virtuelle Methoden in einer übergeordneten Klasse überschreiben, erfordern das override-Schlüsselwort als Möglichkeit, eine versehentliche Neudefinition zu verhindern. In C# verhält sich eine Struktur wie eine vereinfachte Klasse. Sie entspricht einem auf dem Stapel reservierten Typ, der Schnittstellen implementieren kann, jedoch keine Vererbung unterstützt. C# bietet die Typen record class und record struct, deren Zweck primär im Speichern von Datenwerten besteht.

Klassen und Objekte

Klassen sind die grundlegendsten der C#-Typen. Eine Klasse ist eine Datenstruktur, die einen Zustand (Felder) und Aktionen (Methoden und andere Funktionsmember) in einer einzigen Einheit kombiniert. Eine Klasse stellt eine Definition für Instanzen der Klasse bereit, die auch Objekte genannt werden. Klassen unterstützen Vererbung und Polymorphie. Dies sind Mechanismen, durch die abgeleitete Klassen erweitert und Basisklassen spezialisiert werden können.

Neue Klassen werden mithilfe von Klassendeklarationen erstellt. Eine Klassendeklaration beginnt mit einem Header. Der Header legt Folgendes fest:

  • Die Attribute und Modifizierer der Klasse
  • Den Namen der Klasse
  • Die Basisklasse (wenn von einer Basisklasse geerbt wird)
  • Die von der Klasse implementierten Schnittstellen

Auf den Header folgt der Klassenkörper. Dieser besteht aus einer Liste der Memberdeklarationen, die zwischen den Trennzeichen { und } eingefügt werden.

Im folgenden Code wird die Deklaration einer einfachen Klasse namens Point veranschaulicht:

public class Point
{
    public int X { get; }
    public int Y { get; }
    
    public Point(int x, int y) => (X, Y) = (x, y);
}

Instanzen von Klassen werden mit dem new-Operator erstellt. Dieser reserviert Speicher für eine neue Instanz, ruft einen Konstruktor zum Initialisieren der Instanz auf und gibt einen Verweis auf die Instanz zurück. Mit den folgenden Anweisungen werden zwei Point-Objekte erstellt und Verweise auf diese Objekte in zwei Variablen gespeichert:

var p1 = new Point(0, 0);
var p2 = new Point(10, 20);

Der von einem Objekt belegte Speicher wird automatisch wieder freigegeben, wenn das Objekt nicht mehr erreichbar ist. Es ist weder erforderlich noch möglich, die Zuweisung von Objekten in C# explizit aufzuheben.

Typparameter

Typparameter werden von generischen Klassen definiert. Typparameter sind eine Liste von Typparameternamen, die in spitzen Klammern enthalten sind. Typparameter folgen auf den Klassennamen. Die Typparameter können dann im Körper der Klassendeklarationen zum Definieren der Klassenmember verwendet werden. Im folgenden Beispiel lauten die Typparameter von PairTFirst und TSecond:

public class Pair<TFirst, TSecond>
{
    public TFirst First { get; }
    public TSecond Second { get; }
    
    public Pair(TFirst first, TSecond second) => 
        (First, Second) = (first, second);
}

Ein Klassentyp, der zum Akzeptieren von Typparametern deklariert wird, wird als generischer Klassentyp bezeichnet. Struktur-, Schnittstellen- und Delegattypen können auch generisch sein. Wenn die generische Klasse verwendet wird, müssen für jeden der Typparameter Typargumente angegeben werden:

var pair = new Pair<int, string>(1, "two");
int i = pair.First;     //TFirst int
string s = pair.Second; //TSecond string

Ein generischer Typ, für den Typargumente angegeben wurden (siehe Pair<int,string> oben), wird als konstruierter Typ bezeichnet.

Basisklassen

Mit einer Klassendeklaration kann eine Basisklasse angegeben werden. Fügen Sie nach dem Klassennamen und den Typparametern einen Doppelpunkt und den Namen der Basisklasse ein. Das Auslassen einer Basisklassenspezifikation ist dasselbe wie eine Ableitung vom Typ object. Im folgenden Beispiel ist Point die Basisklasse von Point3D. Im folgenden ersten Beispiel ist object die Basisklasse von Point:

public class Point3D : Point
{
    public int Z { get; set; }
    
    public Point3D(int x, int y, int z) : base(x, y)
    {
        Z = z;
    }
}

Eine Klasse erbt die Member der zugehörigen Basisklasse. Vererbung bedeutet, dass eine Klasse implizit nahezu alle Member der Basisklasse enthält. Eine Klasse erbt die Instanz- und statischen Konstruktoren sowie den Finalizer nicht. Eine abgeleitete Klasse kann den geerbten Membern neue Member hinzufügen, aber die Definition eines geerbten Members kann nicht entfernt werden. Im vorherigen Beispiel erbt Point3D die Member X und Y von Point, und alle Point3D-Instanzen enthalten die drei Eigenschaften X, Y und Z.

Ein Klassentyp kann implizit in einen beliebigen zugehörigen Basisklassentyp konvertiert werden. Eine Variable eines Klassentyps kann auf eine Instanz der Klasse oder eine Instanz einer beliebigen abgeleiteten Klasse verweisen. Beispielsweise kann in den vorherigen Klassendeklarationen eine Variable vom Typ Point entweder auf Point oder auf Point3D verweisen:

Point a = new(10, 20);
Point b = new Point3D(10, 20, 30);

Strukturen

Klassen definieren Typen, die die Vererbung und die Polymorphie unterstützen. Sie ermöglichen es Ihnen, komplexe Verhaltensweise anhand von Hierarchien abgeleiteter Klassen zu erstellen. Im Gegensatz dazu sind struct-Typen einfachere Typen, deren Hauptaufgabe das Speichern von Datenwerten ist. Strukturen können keinen Basistyp deklarieren, sie leiten implizit von System.ValueType ab. Sie können keine anderen struct-Typen von einem struct-Typ ableiten. Sie werden implizit versiegelt.

public struct Point
{
    public double X { get; }
    public double Y { get; }
    
    public Point(double x, double y) => (X, Y) = (x, y);
}

Schnittstellen

Eine Schnittstelle definiert einen Vertrag, der von Klassen und Strukturen implementiert werden kann. Sie definieren eine Schnittstelle, um Funktionen zu deklarieren, die von verschiedenen Typen gemeinsam genutzt werden. Die System.Collections.Generic.IEnumerable<T>-Schnittstelle definiert beispielsweise eine konsistente Methode zum Durchlaufen aller Elemente in einer Sammlung, z. B. in einem Array. Eine Schnittstelle kann Methoden, Eigenschaften, Ereignisse und Indexer enthalten. Eine Schnittstelle stellt in der Regel keine Implementierungen der von ihr definierten Member bereit. Sie gibt lediglich die Member an, die von Klassen oder Strukturen bereitgestellt werden müssen, die die Schnittstelle implementieren.

Schnittstellen können Mehrfachvererbung einsetzen. Im folgenden Beispiel erbt die Schnittstelle IComboBox sowohl von ITextBox als auch IListBox.

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

interface IListBox : IControl
{
    void SetItems(string[] items);
}

interface IComboBox : ITextBox, IListBox { }

Klassen und Strukturen können mehrere Schnittstellen implementieren. Im folgenden Beispiel implementiert die Klasse EditBox sowohl IControl als auch IDataBound.

interface IDataBound
{
    void Bind(Binder b);
}

public class EditBox : IControl, IDataBound
{
    public void Paint() { }
    public void Bind(Binder b) { }
}

Wenn eine Klasse oder Struktur eine bestimmte Schnittstelle implementiert, können Instanzen dieser Klasse oder Struktur implizit in diesen Schnittstellentyp konvertiert werden. Beispiel:

EditBox editBox = new();
IControl control = editBox;
IDataBound dataBound = editBox;

Enumerationen

EinEnum-Typ definiert mehrere konstante Werte. Mit dem folgenden enum-Typ werden Konstanten deklariert, die verschiedene Wurzelgemüsesorten definieren:

public enum SomeRootVegetable
{
    HorseRadish,
    Radish,
    Turnip
}

Sie können einen enum-Typ definieren, der in Kombination als Flags verwendet werden sollen. In der folgenden Deklaration werden Flags für die vier Jahreszeiten deklariert. Jede Kombination der Jahreszeiten kann angewendet werden, einschließlich eines All-Werts, der alle Jahreszeiten umfasst:

[Flags]
public enum Seasons
{
    None = 0,
    Summer = 1,
    Autumn = 2,
    Winter = 4,
    Spring = 8,
    All = Summer | Autumn | Winter | Spring
}

Im folgenden Beispiel werden Deklaration der beiden Enumerationen veranschaulicht:

var turnip = SomeRootVegetable.Turnip;

var spring = Seasons.Spring;
var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;

Nullable-Typen

Jede Art von Variable kann als non-nullable oder nullable deklariert werden. Eine Nullable-Variable kann einen zusätzlichen null-Wert enthalten, der angibt, dass kein Wert vorliegt. Nullable-Werttypen (Strukturen oder Enumerationen) werden mit System.Nullable<T> dargestellt. Non-Nullable- und Nullable-Verweistypen werden beide vom zugrunde liegenden Verweistyp repräsentiert. Der Unterschied wird von Metadaten dargestellt, die vom Compiler und einigen Bibliotheken gelesen werden. Der Compiler gibt Warnungen aus, wenn Nullable-Verweise dereferenziert werden, ohne dass ihr Wert zunächst auf null geprüft wird. Der Compiler gibt auch Warnungen aus, wenn Non-Nullable-Verweisen ein Wert zugewiesen wird, der null sein kann. Im folgenden Beispiel wird ein nullable int-Wert deklariert und mit null initialisiert. Dann wird der Wert auf 5 festgelegt. Dasselbe Konzept wird mit nullable string veranschaulicht. Weitere Informationen finden Sie unter Nullable-Werttypen und Nullable-Verweistypen.

int? optionalInt = default; 
optionalInt = 5;
string? optionalText = default;
optionalText = "Hello World.";

Tupel

C# unterstützt Tupel die eine kompakte Syntax zum Gruppieren mehrerer Datenelemente in einer einfachen Datenstruktur bereitstellen. Sie können ein Tupel instanziieren, indem Sie die Typen und Namen der Member zwischen ( und ) deklarieren. Dies wird im folgenden Beispiel veranschaulicht:

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
//Output:
//Sum of 3 elements is 4.5.

Tupel bieten eine Alternative für Datenstrukturen mit mehreren Membern, ohne dass die Bausteine verwendet werden müssen, die im nächsten Artikel beschrieben werden.