Generics
Generische Klassen erklärt anhand von einem Stack
In C# können Generics verwendet werden, um Code zu schreiben, der mit verschiedenen Datentypen arbeiten kann, ohne dass der Code für jeden Datentyp neu geschrieben werden muss. Ein Beispiel dafür ist die Implementierung eines Stack-Datentyps.
Ein Stack ist ein Datentyp, bei dem der letzte Wert, der hinzugefügt wurde, als erster Wert wieder entfernt wird. Eine einfache Implementierung eines Stacks mit Generics in C# könnte wie folgt aussehen:
public class Stack<T>
{
private List<T> stackList;
public Stack()
{
stackList = new List<T>();
}
public void Push(T value)
{
stackList.Add(value);
}
public T Pop()
{
if (stackList.Count == 0)
{
throw new InvalidOperationException("Stack is empty");
}
T value = stackList[stackList.Count - 1];
stackList.RemoveAt(stackList.Count - 1);
return value;
}
}
In diesem Beispiel wird die generische Klasse Stack definiert, die einen generischen Typ T
verwendet. Das bedeutet, dass der Stack mit jedem beliebigen Datentyp verwendet werden kann, solange der Datentyp T
unterstützt wird.
Der Stack verwendet eine interne List<T>
zur Speicherung der Elemente. Die Methode Push
fügt ein neues Element zum Stack hinzu, während die Methode Pop
das oberste Element vom Stack entfernt und zurückgibt. Wenn der Stack leer ist, wird eine InvalidOperationException
ausgelöst.
Hier ist ein Beispiel, wie man einen Stack mit dem generischen Datentyp int erstellt und einige Elemente hinzufügt und entfernt:
Stack<int> intStack = new Stack<int>();
intStack.Push(10);
intStack.Push(20);
intStack.Push(30);
Console.WriteLine(intStack.Pop()); // gibt 30 aus
Console.WriteLine(intStack.Pop()); // gibt 20 aus
Console.WriteLine(intStack.Pop()); // gibt 10 aus
In diesem Beispiel wird ein Stack mit dem generischen Datentyp int
erstellt und die Werte 10, 20 und 30 hinzugefügt. Dann werden die Werte mit der Pop
-Methode in umgekehrter Reihenfolge entfernt und ausgegeben.
Where Klausel (Constraints) bei Generics
Eine der nützlichen Funktionen von Generics in C# sind die Where-Klauseln, mit denen Einschränkungen für die generischen Typen angegeben werden können. Dadurch kann die Flexibilität der Generics beibehalten werden, aber es können trotzdem Regeln für die Typen festgelegt werden, die in der generischen Klasse oder Methode verwendet werden können.
Eine Where-Klausel wird nach der generischen Deklaration platziert und gibt an, welche Einschränkungen für die generischen Typen gelten. Zum Beispiel kann die Where-Klausel verwendet werden, um sicherzustellen, dass der generische Typ ein Klassenname oder ein Interface ist, das eine bestimmte Methode oder Eigenschaft implementiert. Die Where-Klausel kann auch verwendet werden, um eine Einschränkung für einen bestimmten Basistyp oder eine Schnittstelle festzulegen, von der der generische Typ abgeleitet werden muss.
Ein einfaches Beispiel für eine Where-Klausel könnte wie folgt aussehen:
public class Beispiel<T>
where T : class
{
//...
}
In diesem Beispiel wird eine generische Klasse "Beispiel" deklariert, die einen generischen Typ "T" verwendet. Die Where-Klausel "where T : class" stellt sicher, dass "T" eine Referenztyp-Klasse ist, d.h. dass "T" nicht "null" sein kann und dass "T" eine Basisklasse oder ein Interface implementieren kann.
Übersicht where-Klausel (Constraints) Möglichkeiten
Where-Klausel | Bedeutung |
---|---|
where T : struct | T muss ein Werttyp (Value Type) sein |
where T : class | T muss ein Referenztyp (Reference Type) sein |
where T : new() | T muss einen öffentlichen, parameterlosen Konstruktor haben |
where T : <Basis-Klasse> | T muss von der angegebenen Klasse erben |
where T : <Interface> | T muss das angegebene Interface implementieren |
where T : <Klasse> , <Interface1> , <Interface2> | T muss von der angegebenen Klasse erben und die angegebenen Interfaces implementieren |
where T : U | T muss von U erben (U ist eine Basis-Klasse oder ein Interface) |
Where-Klauseln können auch kombiniert werden, um die Einschränkungen weiter zu spezifizieren, zum Beispiel:
public class MyClass<T> where T : class, IComparable<T>, new()
return default
Das Schlüsselwort "default" wird verwendet, um den Standardwert für einen bestimmten Typ in C# zurückzugeben. Der Standardwert ist 0 für numerische Typen, false für boolsche Typen und null für Referenztypen. Das ist wichtig, da man den Rückgabetyp aufgrund der dynamic von Generics nicht konkret vorhersagen kann.
Generische Methoden
Eine generische Methode ist eine Methode, die mit einem oder mehreren generischen Typparametern definiert wird, wodurch sie für verschiedene Datentypen wiederverwendet werden kann. Die Typen in der generischen Methode dürfen sich unterscheiden von den generischen Typen der Klasse. Weiterhin kann bei generischen Typen auch die Constraints Einschränkung genutzt werden.
public T Max<T>(T a, T b) where T : IComparable<T>
{
if (a.CompareTo(b) > 0)
{
return a;
}
else
{
return b;
}
}
Diese Methode vergleicht zwei Objekte vom generischen Typ T
, der durch den Constraint where T : IComparable<T>
eingeschränkt ist, und gibt das größere der beiden Objekte zurück. Der Constraint stellt sicher, dass die Methode nur für Typen aufgerufen werden kann, die das IComparable<T>
-Interface implementieren und somit eine Methode CompareTo
zur Verfügung stellen, mit der die Objekte verglichen werden können.
Nullable Typen
Über einen Nullable struct ist es möglich Zahlenwerten auch einen null Wert zuzuweisen.
Nullable<int> number = null;
//Kurzschreibweise
int? number2 = null;
Nicht Nullable Variablen können implizit in Nullable Variablen umgewandelt werden.
int number3 = 1637;
int? number4 = number3;
Wenn Nullable Variablen in nicht Nullable Variablen umgewandelt werden Bedarf es einen expliziten Konvertierung.
int? number5 = 1637;
int number6 = (int)number5;
// Bessere Variante, da es sonst bei einem null Value zu einer InvalidOperationException kommen würde
int? number7 = null;
int number8 = number7 ?? -1;
Generische Collections
Der Vorteil von generischen Collections in C# ist, dass sie Typsicherheit bieten. Das bedeutet, dass sie nur Elemente desselben Typs aufnehmen können, was unerwünschte Laufzeitfehler vermeidet und die Lesbarkeit des Codes verbessert. Außerdem können generische Collections bei der Verwendung von Werttypen dazu beitragen, unnötige Box- und Unboxing-Vorgänge zu vermeiden, was die Leistung verbessert.
Collection | Beschreibung |
---|---|
List<T> | Eine dynamisch wachsende Liste von Objekten vom Typ T . |
LinkedList<T> | Eine doppelt verlinkte Liste von Objekten vom Typ T . |
Queue<T> | Eine First-In-First-Out (FIFO) Warteschlange von Objekten vom Typ T . |
Stack<T> | Eine Last-In-First-Out (LIFO) Stapel von Objekten vom Typ T . |
Dictionary<TKey, TValue> | Eine Sammlung von Schlüssel-Wert-Paaren, wobei jeder Schlüssel vom Typ TKey und jeder Wert vom Typ TValue ist. |
SortedList<TKey, TValue> | Eine Sammlung von Schlüssel-Wert-Paaren, die nach dem Schlüssel sortiert sind, wobei jeder Schlüssel vom Typ TKey und jeder Wert vom Typ TValue ist. |
HashSet<T> | Eine Sammlung von eindeutigen Objekten vom Typ T . |
SortedSet<T> | Eine sortierte Sammlung von eindeutigen Objekten vom Typ T . |
ObservableCollection<T> | Eine spezielle List<T> , die Ereignisse auslöst, wenn sich ihre Elemente ändern. |