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"; // Error

Warum? Weil numberBox eine Box für int ist, aber "Hello" ein string ist.


5. Warum sind Generics nützlich?

Generics haben mehrere Vorteile:

  1. Man muss weniger Code schreiben.
  2. Der Code kann für viele Datentypen verwendet werden.
  3. C# erkennt falsche Datentypen schon beim Programmieren.
  4. 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:

  • TFirst ein Platzhalter für string
  • TSecond ein Platzhalter für int

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 runtime

Das 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"); // Error

Der 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()); // Second

Verwendung 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()); // 20

Die 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
TSecond

Beispiele:

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"; // Error

Die Box erlaubt nur int.


Fehler 2: Typ beim Erstellen vergessen

Box box = new Box(); // Error

Richtig 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; // Error

box.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 Content haben
  • einen Konstruktor haben, der Content setzt
  • 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 IsSuccess vom Typ bool

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());  // A

18. 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.