Was sind Generics in C# und wozu dienen sie
Stellen Sie sich vor, Sie schreiben eine Methode, die mit verschiedenen Datentypen arbeiten soll: ganzen Zahlen, Zeichenketten, Objekten. Ohne Generics (Generika) müssten Sie entweder den Code für jeden Typ duplizieren oder object mit anschließender Typumwandlung verwenden. Beide Ansätze führen zu Problemen: der erste zu Redundanz, der zweite zu Laufzeitfehlern.
Generics (generische Typen) sind ein Mechanismus in C#, der es ermöglicht, Klassen, Methoden, Schnittstellen und Delegaten zu erstellen, die mit jedem Datentyp arbeiten, während die strenge Typisierung erhalten bleibt. Anstatt Code für einen bestimmten Typ zu schreiben, verwenden Sie einen Typparameter (normalerweise mit dem Buchstaben T bezeichnet), der bei der Verwendung durch den tatsächlichen Typ ersetzt wird.
Die Hauptvorteile von Generics:
- Typsicherheit – der Compiler überprüft die korrekte Verwendung der Typen bereits zur Kompilierzeit
- Leistung – kein Boxing/Unboxing bei Werttypen
- Wiederverwendbarkeit von Code – eine Klasse/Methode arbeitet mit vielen Typen
- Lesbarkeit – der Code wird sauberer und verständlicher
Das Problem ohne Generics: Beispiel mit object
Vor der Einführung von Generics verwendeten Entwickler oft den Typ object, um universelle Methoden zu erstellen. Betrachten wir ein Beispiel:
public class Stack{ private object[] _items = new object[10]; private int _count = 0; public void Push(object item) { _items[_count++] = item; } public object Pop() { return _items[--_count]; } // Verwendung Stack stack = new Stack(); stack.Push(123); // Boxing von int in object stack.Push("hello"); // Jeder Typ kann hinzugefügt werden int number = (int)stack.Pop(); // Unboxing und Typumwandlung}Die Probleme sind offensichtlich:
- Beim Abrufen ist eine explizite Typumwandlung erforderlich
- Es kann versehentlich ein Wert eines anderen Typs hinzugefügt werden
- Bei Werttypen (int, double) erfolgt Boxing/Unboxing, was die Leistung beeinträchtigt
Grundlagen der Arbeit mit Generics in C#
Erstellen einer generischen Klasse
Betrachten wir dasselbe Beispiel eines Stapels, jedoch mit Generics:
public class Stack<T>{ private T[] _items = new T[10]; private int _count = 0; public void Push(T item) { _items[_count++] = item; } public T Pop() { return _items[--_count]; }}
// VerwendungStack<int> intStack = new Stack<int>();intStack.Push(123);intStack.Push(456);int number = intStack.Pop(); // Kein Unboxing nötig, Typ ist bekannt
Stack<string> stringStack = new Stack<string>();stringStack.Push("Hello");// stringStack.Push(123); // Kompilierfehler! TypsicherheitDer Parameter <T> wird in spitzen Klammern angegeben. Beim Erstellen einer Instanz geben Sie den konkreten Typ an, und der Compiler generiert eine spezialisierte Version der Klasse für diesen Typ.
Generische Methoden
Generics funktionieren nicht nur mit Klassen, sondern auch mit einzelnen Methoden:
public class Utils{ public static T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; }}
// Verwendungint maxInt = Utils.Max(5, 10); // 10string maxStr = Utils.Max("apple", "banana"); // "banana"Einschränkungen (Constraints) bei Generics
Manchmal müssen die Typen eingeschränkt werden, die mit einem Generikum verwendet werden können. Dazu verwendet man Einschränkungen (Constraints) mit dem Schlüsselwort where:
// Einschränkung: T muss ein Referenztyp seinpublic class Repository<T> where T : class{