1. Kurze Vorgeschichte
In alten Zeiten gab es in C# nur zwei Freuden beim Arbeiten mit Collections: Arrays benutzen oder sogenannte "schwach typisierte" Collections – wie ArrayList, wo man Objekte jeglichen Typs reinschmeißen konnte. Klingt nach Freiheit! Aber sobald du in so eine Collection einen String und eine ganze Zahl zusammenpackst – geht das Chaos los: Du kannst kein Element mehr sicher rausziehen, ohne zusätzliche (und nicht immer funktionierende) Typprüfungen. Und dann – fiese Fehler aus dem Nichts und wildes Rumgecasten (Typumwandlungen).
Beispiel für eine nicht-generische Collection (ArrayList):
using System.Collections;
ArrayList zeug = new ArrayList();
zeug.Add(42);
zeug.Add("Hallo C#");
int zahl = (int)zeug[0]; // OK
string text = (string)zeug[1]; // OK
int fail = (int)zeug[1]; // BUMM! InvalidCastException (Laufzeitfehler)
Ja, der Compiler meckert nicht – der Fehler kommt erst zur Laufzeit! Das ist wie den Kühlschrank aufmachen und plötzlich einen heißen Kochtopf drin finden – Überraschung!
Worum geht's bei Generics?
Generische Collections (Generics) wurden eingeführt, damit man Collections mit garantiertem Inhaltstyp bauen kann. Das bringt drei Hauptvorteile:
- Typsicherheit schon beim Kompilieren. Der Compiler lässt dich nicht aus Versehen was Falsches in die Collection packen.
- Bequemlichkeit: Du musst nicht jedes Mal den Typ manuell casten.
- Performance: Keine unnötige "Boxing und Unboxing" von Value Types (boxing/unboxing).
2. Was sind Generics überhaupt?
Generics – das ist der magische Trick, Datenstrukturen und Methoden so zu beschreiben, dass sie mit jedem Typ funktionieren, aber trotzdem streng typisiert bleiben.
Stell dir eine Universalbox vor, die du sowohl für Bücher als auch für Socken benutzen kannst, aber immer nur für einen Typ auf einmal. Wenn die Box als "nur für Bücher" deklariert ist – kann niemand Socken reinschmuggeln. Genauso bei generic-Collections: Wenn du eine Collection aus int baust, kommt da nicht aus Versehen ein string rein.
Beispiel für eine generische Klasse
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Hallo Generics!" };
T – das ist der "Typ-Parameter", der angibt, womit deine Box arbeitet. C# verlangt, dass du immer klar sagst, welchen Typ du reinpackst.
Generische Collections im .NET
Im .NET Framework haben praktisch alle modernen Collections eine generische Version. Das sind:
- List<T> – eine dynamische Liste von Elementen vom Typ T.
- Dictionary<TKey, TValue> – ein assoziatives Array (Dictionary) mit Keys und Values.
- Queue<T>, Stack<T> – Queues und Stacks.
- und viele andere.
3. Wie funktioniert das: Aufbau von Generics
Typ-Parameter
Wenn du eine Collection wie List<int> deklarierst, erzeugt der C#-Compiler eine eigene Variante (Spezialisierung) dieser Klasse speziell für den Typ int. Wenn du dann List<string> deklarierst, macht der Compiler noch eine Variante, aber diesmal für Strings usw.
Für dich als Entwickler fühlt sich das wie Magie an:
List<int> zahlen = new List<int>();
zahlen.Add(1); // Nur int erlaubt
List<string> woerter = new List<string>();
woerter.Add("hallo"); // Nur string erlaubt
Wenn du versuchst, einen anderen Typ reinzupacken, meckert der Compiler sofort:
zahlen.Add("fail"); // FEHLER beim Kompilieren!
Typsicherheit: Compile-time vs. Run-time
Das heißt, Fehler wie oben kommen gar nicht erst bis zur Laufzeit (run-time). Deine Collection ist so gut geschützt, da kommt keiner ran – der Compiler steht wie ein Cerberus vor der Tür.
Unter der Haube
- Für Referenztypen (z.B. string, object) gibt's keine unnötigen Casts mehr.
- Für Value Types (int, double) verschwindet das Problem mit "Boxing/Unboxing" (boxing/unboxing).
- Generics in .NET führen nicht zu Code-Bloat – die CLR optimiert das beim JIT-Compile.
4. Wie Generics die Performance steigern
Lass uns nochmal die magischen Eigenschaften hervorheben, die Generics unserem Code schenken:
Typsicherheit (Type Safety):
Das ist wohl das Wichtigste. Dank Generics wird der Compiler zu deinem persönlichen Bodyguard, der keine "Fremden" in deine Collection lässt. Du kannst dir absolut sicher sein, dass List<Product> nur Product-Objekte enthält, und nicht irgendwelche zufälligen Strings oder Zahlen. Das eliminiert eine ganze Klasse von Fehlern, die früher erst zur Laufzeit aufgetaucht sind und zu unerwarteten "Explosionen" (InvalidCastException) geführt haben. Dein Code wird zuverlässiger und vorhersehbarer.
Performance:
Wie schon gesagt, für Value Types (wie int, double oder DateTime) ermöglichen Generics einen effizienteren Umgang mit Speicher und CPU-Zeit. Für Collections, die Millionen von Elementen speichern oder oft geändert werden, kann das echt entscheidend sein. Anstatt ständig Orangen aus der Kiste in den Sack und zurück zu packen, legst du sie einfach direkt in die richtige Kiste.
Code-Wiederverwendung (Code Reusability):
Mit Generics kannst du denselben Code für verschiedene Datentypen schreiben, ohne ihn zu duplizieren. Angenommen, du brauchst eine Funktion, um zwei Variablen zu tauschen. Ohne Generics müsstest du SwapInt(ref int a, ref int b), SwapString(ref string a, ref string b), SwapProduct(ref Product a, ref Product b) usw. schreiben. Mit Generics reicht eine Funktion: Swap<T>(ref T a, ref T b). Das gilt auch für Collections: Du brauchst keine IntList, StringList, ProductList – List<T> reicht. Dein Code wird kompakter, leichter wartbar und skalierbar.
5. Generische Methoden und eigene Generic-Klassen
Generische Methoden
Generics – das ist nicht nur für Collections! Du kannst auch eigene generische Methoden schreiben, die mit jedem Typ funktionieren. Das macht den Code viel universeller.
//Typ-Parameter T direkt nach dem Methodennamen angeben
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// Verwendung:
int a = 10, b = 20;
Swap(ref a, ref b); // Jetzt a == 20, b == 10
string eins = "eins", zwei = "zwei";
Swap(ref eins, ref zwei); // Funktioniert auch für Strings!
Achte darauf, dass der C#-Compiler den Typ-Parameter der Methode selbst aus den übergebenen Variablen ableiten kann. Deshalb musst du im Beispiel oben den Typ bei Swap nicht extra angeben.
Eigene Generic-Klassen
Du kannst sogar deine eigenen "Boxen" (Generic-Klassen) bauen. Das ist einfacher als du denkst:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// Verwendung:
var paar = new Pair<int, string> { First = 42, Second = "antwort" };
6. Beispiel für die Nutzung von Generic-Collections
Kehren wir zurück zu unserer "To-Do-Liste", die wir in den Beispielen der letzten Vorlesungen gebaut haben. Früher haben wir Aufgaben in einem Array gespeichert oder einfach auf dem Bildschirm ausgegeben. Jetzt speichern wir sie dynamisch in einer List<string>.
Beispiel: Aufgaben dynamisch zur Liste hinzufügen
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> aufgaben = new List<string>();
Console.WriteLine("Gib eine Aufgabe ein (oder eine leere Zeile zum Beenden):");
string eingabe;
while (!string.IsNullOrWhiteSpace(eingabe = Console.ReadLine()))
{
aufgaben.Add(eingabe);
Console.WriteLine("Aufgabe hinzugefügt! Gib noch eine ein (oder eine leere Zeile zum Beenden):");
}
Console.WriteLine("\nDeine Aufgabenliste für heute:");
foreach (string aufgabe in aufgaben)
{
Console.WriteLine("- " + aufgabe);
}
}
}
Was ist hier cool?
- Die Collection wächst automatisch, wenn du neue Aufgaben hinzufügst.
- Du kannst nicht aus Versehen was anderes als einen String in die Liste packen.
- Du kannst die Liste easy durchgehen und ihren Inhalt ausgeben.
7. Typische Anfängerfehler, Besonderheiten und Tipps
Der Umstieg von nicht-generischen auf generische Collections kann verwirrend sein. Hier ein paar typische Situationen, auf die Anfänger stoßen:
- Versuch, ein Element vom falschen Typ hinzuzufügen (List<int> zahlen = new List<int>(); zahlen.Add("hi"); // Fehler).
- Man will verschiedene Typen in einer Collection mischen – dann muss man einen Basistyp wählen (z.B. List<object>) – und dann immer wieder zurückcasten.
- Man vergisst, dass es constraints gibt, schreibt zu "breite" Generics und fängt komische Fehler beim Kompilieren ein.
GO TO FULL VERSION