Generische Datentypen in C#
1. Was bedeutet „generisch“?
In C# bedeutet generisch, dass eine Klasse, Methode oder Datenstruktur mit verschiedenen Datentypen arbeiten kann, ohne dass man den Code mehrfach schreiben muss.
Statt also eine Klasse nur für int, nur für string oder nur für double zu schreiben, kann man eine allgemeine Version schreiben.
Diese allgemeine Version verwendet einen Platzhalter für einen Datentyp.
Dieser Platzhalter heißt oft T.
T steht für Type, also „Datentyp“.
2. Das Problem ohne Generics
Stellen wir uns vor, wir wollen eine Box speichern, in der ein Wert liegt.
Für eine Zahl könnten wir diese Klasse schreiben:
public class IntBox
{
public int Value { get; set; }
}Für Text bräuchten wir eine zweite Klasse:
public class StringBox
{
public string Value { get; set; }
}Das Problem: Der Code ist fast gleich. Nur der Datentyp ist anders.
Wenn wir noch double, bool oder eigene Klassen speichern wollen, müssten wir immer wieder neue Klassen schreiben.
Das ist umständlich.
3. Die Lösung: Eine generische Klasse
Mit Generics schreiben wir nur eine einzige Klasse:
public class Box<T>
{
public T Value { get; set; }
}Hier ist T ein Platzhalter für einen Datentyp.
Beim Verwenden der Klasse legen wir fest, welcher Datentyp eingesetzt wird.
Beispiel mit int:
Box<int> numberBox = new Box<int>();
numberBox.Value = 42;
Console.WriteLine(numberBox.Value);Beispiel mit string:
Box<string> textBox = new Box<string>();
textBox.Value = "Hello";
Console.WriteLine(textBox.Value);Die Klasse Box<T> bleibt gleich. Nur T wird beim Verwenden durch einen echten Datentyp ersetzt.
4. Was passiert bei Box<int>?
Wenn wir schreiben:
Box<int> numberBox = new Box<int>();Dann bedeutet das:
Erstelle eine Box, in der nur
int-Werte gespeichert werden dürfen.
Dadurch verhindert C# Fehler.
Das hier funktioniert:
Box<int> numberBox = new Box<int>();
numberBox.Value = 10;Das hier funktioniert nicht:
Box<int> numberBox = new Box<int>();
numberBox.Value = "Hello"; // ErrorWarum? Weil numberBox eine Box für int ist, aber "Hello" ein string ist.
5. Warum sind Generics nützlich?
Generics haben mehrere Vorteile:
- Man muss weniger Code schreiben.
- Der Code kann für viele Datentypen verwendet werden.
- C# erkennt falsche Datentypen schon beim Programmieren.
- Der Code wird übersichtlicher und wiederverwendbarer.
Generics helfen also dabei, Klassen zu schreiben, die flexibel, aber trotzdem sicher sind.
6. Eine einfache generische Klasse mit Konstruktor
Eine generische Klasse kann natürlich auch einen Konstruktor haben.
public class Box<T>
{
public T Value { get; set; }
public Box(T value)
{
Value = value;
}
}Verwendung:
Box<int> numberBox = new Box<int>(100);
Box<string> textBox = new Box<string>("Apple");
Console.WriteLine(numberBox.Value);
Console.WriteLine(textBox.Value);Hier bekommt die Box direkt beim Erstellen einen Startwert.
7. Generische Klassen mit Methoden
Eine generische Klasse kann auch Methoden enthalten.
public class Storage<T>
{
private T item;
public void Save(T newItem)
{
item = newItem;
}
public T Load()
{
return item;
}
}Verwendung:
Storage<string> nameStorage = new Storage<string>();
nameStorage.Save("Anna");
string name = nameStorage.Load();
Console.WriteLine(name);Hier wird T durch string ersetzt.
Das bedeutet:
public void Save(string newItem)und:
public string Load()C# denkt sich das automatisch aus, sobald wir Storage<string> schreiben.
8. Eigene Klassen als generischer Datentyp
Generics funktionieren nicht nur mit einfachen Datentypen wie int oder string.
Man kann auch eigene Klassen verwenden.
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}Jetzt können wir eine Box für einen Schüler erstellen:
Student student = new Student();
student.Name = "Max";
student.Age = 15;
Box<Student> studentBox = new Box<Student>(student);
Console.WriteLine(studentBox.Value.Name);Hier ist T also nicht int oder string, sondern Student.
9. Mehrere generische Datentypen
Eine generische Klasse kann auch mehr als einen Typ-Platzhalter haben.
Beispiel:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
}Verwendung:
Pair<string, int> personAge = new Pair<string, int>("Tom", 16);
Console.WriteLine(personAge.First);
Console.WriteLine(personAge.Second);Hier ist:
TFirstein Platzhalter fürstringTSecondein Platzhalter fürint
Dadurch kann eine Klasse mit mehreren verschiedenen Datentypen arbeiten.
10. Generische Listen: Ein bekanntes Beispiel
In C# habt ihr wahrscheinlich schon Listen gesehen.
Eine Liste ist ein sehr häufiges Beispiel für Generics.
List<int> numbers = new List<int>();
numbers.Add(5);
numbers.Add(10);
numbers.Add(20);
Console.WriteLine(numbers[0]);List<int> bedeutet:
Eine Liste, in der nur
int-Werte gespeichert werden dürfen.
Eine Liste mit Text sieht so aus:
List<string> names = new List<string>();
names.Add("Anna");
names.Add("Ben");
names.Add("Clara");
Console.WriteLine(names[1]);List<string> bedeutet:
Eine Liste, in der nur
string-Werte gespeichert werden dürfen.
11. Warum nicht einfach object verwenden?
In C# kann object fast alles speichern.
Man könnte also schreiben:
public class ObjectBox
{
public object Value { get; set; }
}Dann kann man verschiedene Werte speichern:
ObjectBox box = new ObjectBox();
box.Value = 123;
box.Value = "Hello";Das klingt zuerst praktisch, hat aber Nachteile.
C# weiß dann nicht mehr genau, welcher Datentyp wirklich in der Box steckt.
Dadurch können Fehler leichter passieren.
Beispiel:
ObjectBox box = new ObjectBox();
box.Value = "Hello";
int number = (int)box.Value; // Error at runtimeDas Programm startet vielleicht, aber es stürzt später ab.
Mit Generics passiert das nicht so leicht:
Box<int> box = new Box<int>(123);
int number = box.Value;C# weiß hier genau: In dieser Box ist ein int.
12. Type Safety
Ein wichtiger Begriff bei Generics ist Type Safety.
Das bedeutet:
C# achtet darauf, dass nur passende Datentypen verwendet werden.
Beispiel:
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add("Hello"); // ErrorDer letzte Befehl ist falsch, weil "Hello" kein int ist.
Der Vorteil: Der Fehler wird schon beim Programmieren erkannt, nicht erst später beim Ausführen.
13. Generische Klasse als kleiner Stapel
Ein Stapel funktioniert wie ein Stapel Teller:
- Man legt etwas oben drauf.
- Man nimmt immer das oberste Element wieder weg.
Das nennt man auch Stack.
Hier ist eine sehr einfache generische Stack-Klasse:
public class SimpleStack<T>
{
private List<T> items = new List<T>();
public void Push(T item)
{
items.Add(item);
}
public T Pop()
{
int lastIndex = items.Count - 1;
T item = items[lastIndex];
items.RemoveAt(lastIndex);
return item;
}
public int Count()
{
return items.Count;
}
}Verwendung mit string:
SimpleStack<string> stack = new SimpleStack<string>();
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
Console.WriteLine(stack.Pop()); // Third
Console.WriteLine(stack.Pop()); // SecondVerwendung mit int:
SimpleStack<int> numbers = new SimpleStack<int>();
numbers.Push(10);
numbers.Push(20);
numbers.Push(30);
Console.WriteLine(numbers.Pop()); // 30
Console.WriteLine(numbers.Pop()); // 20Die Klasse SimpleStack<T> funktioniert für beide Datentypen.
Wir mussten sie nur einmal schreiben.
14. Namenskonventionen für generische Typen
Für generische Platzhalter verwendet man oft Namen wie:
T
TItem
TKey
TValue
TFirst
TSecondBeispiele:
public class Box<T>
{
}public class DictionaryEntry<TKey, TValue>
{
}Das T am Anfang zeigt: Das ist ein generischer Typ-Parameter.
15. Typische Fehler
Fehler 1: Falschen Datentyp speichern
Box<int> box = new Box<int>(10);
box.Value = "Hello"; // ErrorDie Box erlaubt nur int.
Fehler 2: Typ beim Erstellen vergessen
Box box = new Box(); // ErrorRichtig ist:
Box<int> box = new Box<int>(10);Oder:
Box<string> box = new Box<string>("Hello");Fehler 3: Erwarteten Datentyp falsch verwenden
Box<string> box = new Box<string>("Hello");
int number = box.Value; // Errorbox.Value ist ein string, kein int.
16. Kurze Zusammenfassung
Generische Klassen verwenden Platzhalter für Datentypen.
Der häufigste Platzhalter heißt T.
Beispiel:
public class Box<T>
{
public T Value { get; set; }
}Beim Verwenden wird T durch einen echten Datentyp ersetzt:
Box<int> numberBox = new Box<int>();
Box<string> textBox = new Box<string>();Generics sind nützlich, weil sie:
- Code wiederverwendbar machen
- Datentypen sicher machen
- Fehler früh erkennen
- Klassen flexibler machen
17. Übungsaufgaben
Aufgabe 1
Erstelle eine generische Klasse Container<T>.
Die Klasse soll:
- eine Eigenschaft
Contenthaben - einen Konstruktor haben, der
Contentsetzt - eine Methode
PrintContent()haben, die den Inhalt ausgibt
Beispiel für die Verwendung:
Container<string> textContainer = new Container<string>("Hello World");
textContainer.PrintContent();
Container<int> numberContainer = new Container<int>(123);
numberContainer.PrintContent();Aufgabe 2
Erstelle eine generische Klasse Result<T>.
Die Klasse soll speichern:
- einen Wert vom Typ
T - eine Eigenschaft
IsSuccessvom Typbool
Beispiel:
Result<int> calculationResult = new Result<int>(42, true);
Result<string> loginResult = new Result<string>("Login failed", false);Aufgabe 3
Erweitere die Klasse SimpleStack<T>.
Füge eine Methode Peek() hinzu.
Diese Methode soll das oberste Element zurückgeben, aber nicht entfernen.
Beispiel:
SimpleStack<string> stack = new SimpleStack<string>();
stack.Push("A");
stack.Push("B");
Console.WriteLine(stack.Peek()); // B
Console.WriteLine(stack.Pop()); // B
Console.WriteLine(stack.Pop()); // A18. Merksatz
Generics erlauben uns, Klassen für verschiedene Datentypen zu schreiben, ohne denselben Code immer wieder zu kopieren.
Oder kurz:
Einmal schreiben, mit vielen Datentypen verwenden.