Generics in C#: warum sie benötigt werden und wie man sie verwendet
Generische Typen (Generics) sind eine der Schlüsselfunktionen der Sprache C#, die es ermöglicht, Klassen, Methoden und Schnittstellen mit parametrisierten Typen zu erstellen. Anstatt denselben Code für jeden Datentyp zu schreiben, schreiben Sie ihn einmal und geben den Typ bei der Verwendung an. Dies verbessert die Leistung, die Typsicherheit und die Wiederverwendbarkeit des Codes. In diesem Artikel werden wir untersuchen, warum Generics benötigt werden, wie man sie in der Praxis anwendet und reale Beispiele aus .NET betrachten.
Warum Generics in C# benötigt werden
Vor dem Aufkommen von Generics in C# (Version 2.0) verwendeten Entwickler häufig den Typ object und die Sammlung ArrayList. Dies führte zu zwei Problemen:
- Leistungsverlust — Boxing und Unboxing für Werttypen (int, double, struct).
- Fehlende Typprüfung zur Kompilierzeit — man konnte versehentlich einen String in eine Liste von Zahlen einfügen und erst zur Laufzeit einen Fehler erhalten.
Generics lösen beide Probleme. Sie ermöglichen die Erstellung typsicherer Datenstrukturen ohne unnötigen Overhead. Beispielsweise arbeitet List<int> mit int ohne Boxing, und der Compiler verhindert das Hinzufügen eines Strings.
Grundlegende Konzepte von Generics
Generische Klassen
Eine generische Klasse wird mit einem Typparameter in spitzen Klammern deklariert. Der Typparameter ist ein Platzhalter, der bei der Erstellung einer Instanz durch den tatsächlichen Typ ersetzt wird.
public class Box<T>{ private T _value;
public Box(T value) { _value = value; }
public T GetValue() => _value;}Verwendung:
var intBox = new Box<int>(42);var stringBox = new Box<string>("Hello");
Console.WriteLine(intBox.GetValue()); // 42Console.WriteLine(stringBox.GetValue());// HelloGenerische Methoden
Eine Methode kann ihren eigenen Typparameter haben, auch wenn die Klasse nicht generisch ist:
public class Utils{ public static void Swap<T>(ref T a, ref T b) { T temp = a; a = b; b = temp; }}
// Verwendungint x = 1, y = 2;Utils.Swap(ref x, ref y);Console.WriteLine($"{x} {y}"); // 2 1Generische Schnittstellen
Schnittstellen mit einem Typparameter ermöglichen die Erstellung flexibler Abstraktionen:
public interface IRepository<T>{ T GetById(int id); void Add(T entity);}
public class UserRepository : IRepository<User>{ public User GetById(int id) => new User(); public void Add(User user) { }}Einschränkungen (Constraints) bei Generics
Manchmal ist es notwendig, Einschränkungen für die Typen festzulegen, die eingesetzt werden können. Dafür wird das Schlüsselwort where verwendet.
Wichtige Arten von Einschränkungen
where T : class— nur Referenztypen (string, Klassen).where T : struct— nur Werttypen (int, DateTime, struct).where T : new()— der Typ muss einen öffentlichen parameterlosen Konstruktor haben.where T : SomeBaseClass— der Typ muss die angegebene Klasse sein oder von ihr erben.where T : ISomeInterface— der Typ muss das Interface implementieren.
Beispiel mit mehreren Einschränkungen:
public class Repository<T> where T : class, IEntity, new(){ public T Create() { return new T(); // erlaubt dank new() }}Kovarianz und Kontravarianz bei Generics
Seit C# 4.0 besteht die Möglichkeit, die Varianz generischer Schnittstellen mit den Schlüsselwörtern in und out zu steuern.
- Kovarianz (out) — ermöglicht die Verwendung