## 7.2 Generics – generische Datentypen In der allgemeinen Programmierung dienen Variablen als Platzhalter für Werte. Die Idee, die hinter den Generics steckt, geht noch einen Schritt weiter. Generics sind ebenfalls Platzhalter, allerdings für Datentypen. Mit Generics lassen sich Klassen und Methoden definieren, für die bestimmte Datentypen zur Entwicklungszeit noch nicht bekannt sind. Lassen Sie mich diese Aussage sofort an dem konkreten Beispiel der benutzerdefinierten Klasse Stack mit den beiden Methoden Push und Pop zeigen. Die Klasse ist hier noch »klassisch« definiert.
    class Stack {
      private readonly int size;
      private Object[] elements;
      private int pointer = 0;
      public Stack(int size) {
        this.size = size;
        elements = new Object[size];
      }
      public void Push(Object element) {
        if (pointer >= this.size)
          throw new StackOverflowException();
        elements[pointer] = element;
        pointer++;
      }
      public Object Pop() {
        pointer--;
        if (pointer >= 0)
          return elements[pointer];
        else {
          pointer = 0;
          throw new InvalidOperationException("Der Stack ist leer");
        }
      }
      public int Length {
        get { return this.pointer; }
      }
    }
Die internen Daten des Stacks werden in einem Object-Array gespeichert. Instanziiert werden kann diese Klasse nur über den Aufruf des einfach parametrisierten Konstruktors, dem ein int übergeben wird, mit dem die Größe des Stacks initialisiert wird. Mit Push wird der Stack-Instanz ein Objekt übergeben. Sollte zu diesem Zeitpunkt die Kapazität des Stacks bereits ausgeschöpft sein, ist eine Ausnahme die Folge. Genauso reagiert auch die Methode Pop, falls sich kein Element mehr auf dem Stack befindet. Wenn ein Element vom Stack geholt wird, wird dieses als ein Element vom Typ Object zurückgegeben. Das ist aber in den meisten Fällen nicht präzise genug, was dazu führt, dass das Element in den richtigen Typ konvertiert werden muss. Das folgende Codefragment soll das verdeutlichen:
    Stack stack = new Stack(10);
    stack.Push(2);
    int str = (int)stack.Pop();
Würden wir versuchen, das vom Stack geholte Element beispielsweise in eine Zeichenfolge zu konvertieren, also
    string str = (string)stack.Pop();
schreiben, würde eine InvalidCastException ausgelöst. Wie Sie sehen können, birgt die Flexibilität des Typs Object gravierende Nachteile in sich. Sie können natürlich versuchen, mehrere Klassen zu entwickeln, die auf einen bestimmten Datentyp spezialisiert sind, z. B. so:
    public class StackInt {
      private int[] elemente
      public void Push(int number) {...}
      ...
    }
Die Typsicherheit wäre damit gewährleistet, daran besteht kein Zweifel. Andererseits führt dieser Ansatz im Extremfall zu einer großen Anzahl ähnlicher Klassen, die jeweils nur für einen spezifischen Typ geeignet sind. Solche Lösungen sind zwar umsetzbar, allerdings auch schlecht zu warten. Zudem könnte sich in naher oder ferner Zukunft der Bedarf nach einem weiteren Stack mit einem noch nicht berücksichtigten Typ ergeben, sodass Sie rückblickend mit dem Ergebnis nicht zufrieden wären. Genau an dieser Stelle spielen Generics ihre ganze Stärke aus. ### 7.2.1 Lösung mit einer generischen Klasse Generics erlauben die Verwendung von Datentypen, die man zum Zeitpunkt der Entwicklung noch nicht festlegen kann oder will. Dazu wird anstelle eines konkreten Datentyps ein Platzhalter angegeben. Der Platzhalter wird in spitzen Klammern angegeben und innerhalb der Klasse wie ein regulärer Datentyp verwendet.
    // -------------------------------------------------------------
    // Beispiel: ...\Kapitel 7\GenerischerStack
    // -------------------------------------------------------------
    class Stack<T> {
      private readonly int size;
      private T[] elements;
      private int pointer = 0;
      public Stack(int size) {
        this.size = size;
        elements = new T[size];
      }
      public void Push(T element) {
        if (pointer >= this.size)
          throw new StackOverflowException();
        elements[pointer] = element;
        pointer++;
      }
      public T Pop() {
        pointer--;
        if (pointer >= 0)
          return elements[pointer];
        else {
          pointer = 0;
          throw new InvalidOperationException("Der Stack ist leer");
        }
      }
      public int Length {
        get { return this.pointer; }
      }
    }
Nutzen Sie eine generische Klasse wie Stack<T>, teilen Sie dem Compiler mit, dass T später durch einen konkreten Datentyp ersetzt werden soll. T wird als generischer Typparameter bezeichnet. Bei der Instanziierung der Klasse Stack<T> ersetzen Sie den generischen Typparameter durch einen konkreten Datentyp. Sie werden feststellen, dass die IntelliSense-Liste bereits Kenntnis vom verwendeten Typ hat und Ihnen diesen auch anbietet. In der folgenden Anweisung handelt es sich um int, was bei der Instanziierung angegeben wird:
    Stack<int> stack = new Stack<int>(10);
Alle Methoden der Klasse, die T als Parameter definieren oder zurückgeben, akzeptieren jetzt nur noch Integer-Werte. Den Zugriff auf die generische Klasse Stack<T> zeigt nachfolgend die Methode Main. Der Code ist in einem try-Block zusammengefasst, um ausgelöste Ausnahmen behandeln zu können. In Kapitel 9, »Fehlerbehandlung und Debugging«, werden wir das Thema Ausnahmen (Exceptions) noch ausgiebig behandeln.
    static void Main(string[] args) {
      try {
        Stack<int> stack = new Stack<int>(10);
        stack.Push(123);
        stack.Push(4711);
        stack.Push(34);
        for (int i = stack.Length; i >= 1; i--) {
          Console.WriteLine(stack.Pop());
        }
        stack.Pop();
      }
      catch (Exception e) {
        Console.WriteLine(e.Message);
      }
      Console.ReadLine();
    }
Hinweis
Die zuvor verwendete generische Klasse Stack<T> definiert einen generischen Datentyp. Je nachdem, wie die zu entwickelnde Klasse aussehen soll, kann der Bedarf an Platzhaltern jedoch variieren. Sie könnten also durchaus eine Klasse definieren, die zwei oder mehr generische Typparameter aufweist. Diese werden innerhalb der spitzen Klammern durch ein Komma voneinander getrennt, z. B.: Demo<A, Z, H>. Hier handelt es sich sogar um die drei generischen Typparameter A, Z und H.
#### Das Schlüsselwort »default« Im Beispiel GenerischerStack wird eine Exception ausgelöst, wenn die Methode Pop aufgerufen wird und der Stack leer ist. Eine andere Lösung hätte vermutlich auch zum Ziel geführt: die Rückgabe mit return.
    public T Pop() {
      pointer--;
      if (pointer >= 0)
        return elements[pointer];
      else {
        pointer = 0;
        return null;
      }
    }
Dieser Ansatz ist richtig, solange der parametrisierte Typ T den Referenztypen zugerechnet werden kann. Handelt es sich jedoch um einen Wertetyp, wird die Laufzeit in einem Desaster enden, da null einem Wertetyp nicht zugewiesen werden kann; die Rückgabe muss dann 0 sein. Andererseits kann bei Referenztypen nicht einfach der Wert 0 zurückgeliefert werden, denn hier muss es null sein. Die Lösung des Problems führt über das C#-Schlüsselwort default. Dieses kann zwischen Referenz- und Wertetypen unterscheiden und liefert null, wenn es sich bei dem konkreten Typ um einen Referenztyp handelt, bzw. 0, wenn es ein Typ ist, der den Wertetypen zugerechnet wird.
    public T Pop() {
      pointer--;
      if (pointer >= 0)
        return elements[pointer];
      else {
        pointer = 0;
        return default(T);
      }
    }
### 7.2.2 Typparameter mit Constraints einschränken Mit der Definition
    public class Stack<T> { ... }
teilen wir dem Compiler mit, dass der Datentyp zur Entwicklungszeit der Klasse Stack noch unbekannt ist. Später kann der generische Typparameter T durch jeden x-beliebigen Datentyp ersetzt werden. Wollen Sie innerhalb des Codes der generischen Klasse jedoch ein bestimmtes Klassenmitglied des verwendeten Typs aufrufen (beispielsweise eine Methode), ist eine explizite und damit auch unsichere Konvertierung notwendig. Fehler, die eventuell auftreten, weil der verwendete Datentyp dieses Klassenmitglied nicht veröffentlicht, würden erst zur Laufzeit der Anwendung erkannt. Um die Problematik zu verstehen, sehen Sie sich das folgende Beispiel an. Die Klasse SortedList<T> hat eine Methode Add, mit der ein Element der Auflistung hinzugefügt wird. Das neue Element soll nach typspezifischen Kriterien eingeordnet werden. Dazu ist ein Vergleich mit den schon enthaltenen Elementen notwendig, zu dem die Methode CompareTo der Schnittstelle IComparable genutzt wird.
    public class SortedList<T> {
      T[] arr = new T[100];
      public T[] Add(T element) {
        for(int i = 0; i < arr.Length; i++) {
          int result = ((IComparable)element).CompareTo(arr[i]);
          ...
        }
        ...
      }
    }
Implementiert der Datentyp IComparable nicht, wird eine Ausnahme ausgelöst. Um diesem Problem aus dem Weg zu gehen, lassen sich die Platzhalter mit Bedingungen (Constraints) ausstatten. Ähnlich einer SQL-Abfrage werden diese nach dem Schlüsselwort where notiert.
    public class SortedList<T> where T : IComparable { ... }
Jetzt ist eine Bedingung festgelegt, an die sich der spätere konkrete Typ halten muss: Er muss die Schnittstelle IComparable unterstützen. In Add können wir sogar auf die explizite Konvertierung verzichten, denn wir schreiben dem Typ T die Implementierung der Schnittstelle vor:
    int result = element.CompareTo(arr[i]);
Eine Bedingung ist nicht nur auf Schnittstellen beschränkt; Sie können auch eine Klasse angeben und legen damit die Basisklasse des an den Typparameter T übergebenen konkreten Typs fest.
Hinweis
Die Angabe ist außerdem nicht nur auf konkrete Typen beschränkt. Sie können auch festlegen, dass der generische Typparameter entweder auf einer class- oder struct-Definition basiert, z. B.:
    class Demo<T> where T : class { ... }
#### Mehrere Constraints definieren Generische Typparameter, die keinen Constraint aufweisen, werden als ungebundene Typparameter bezeichnet; haben sie einen Constraint, nennt man sie gebundene Typparameter. Im Bedarfsfall dürfen Sie auch mehrere Bedingungen angeben, die durch Kommata voneinander getrennt werden.
    public class SortedList<T> where T : IComparable, ICloneable { ... }
Die Schreibweise mit dem Doppelpunkt erinnert an eine Klassenableitung. T muss in unserer Definition die angeführten Schnittstellen implementieren. Sie können auch die Basisklasse von T angeben und anschließend im Programmcode auf die Typkonvertierung verzichten und direkt auf die Mitglieder des parametrisierten Datentyps zugreifen. Es lassen sich auch Bedingungen für mehrere generische Typparameter festlegen. Dazu müssen Sie den Constraint für jeden einzelnen Platzhalter mit where einleiten:
    public class SortedList<T, K>
                        where T : IComparable, ICloneable
                        where K : SomeBaseClass
    { ... }
#### Der Konstruktor-Constraint »new()« Nehmen wir an, Sie möchten in einer generischen Klasse ein Objekt vom Typ eines generischen Typparameters erzeugen. Das Problem dabei ist, dass der C#-Compiler nicht weiß, ob die Klasse, die den Typparameter ersetzt, einen passenden Konstruktor hat. Die Folge wäre ein Kompilierfehler. Um in dieser Situation eine Lösung zu bieten, können Sie an die Liste der Constraints new() anhängen, wie im folgenden Codefragment gezeigt wird:
    public class Demo<K, V> where V : new() {
      public K key = default(K);
      public V value;
      public Demo() {
        value = new V();
      }
    }
In einer Constraint-Liste steht new() grundsätzlich immer am Ende, und Sie treffen damit eine entscheidende Aussage: Der gewählte Argumenttyp muss einen öffentlichen, parameterlosen Konstruktor unterstützen. Einen parametrisierten Konstruktor vorzuschreiben ist nicht möglich. ### 7.2.3 Generische Methoden Generische Typen sind nicht nur im Zusammenhang mit Klassen möglich, sondern auch mit Methoden. Dabei ist es nicht zwingend notwendig, dass die Typparameter einer Methode denen der Klasse entsprechen:
    class GenericClass<T> {
      public void GenericMethod<K>(K obj) { ... }
    }
Im Gültigkeitsbereich der Klasse ist in diesem Fall der Typ T bekannt, K nur innerhalb der Methode. Sie dürfen generische, methodenspezifische Typparameter auch angeben, wenn die Klasse selbst keine definiert:
    class Demo {
      public void GenericMethod<K>(K obj) { ... }
    }
Eine Einschränkung sollten Sie sich aber merken: Eigenschaftsmethoden und Indexer unterstützen nur Typparameter, die sich im Gültigkeitsbereich der Klasse befinden.
Hinweis
Indexer werden im gleichnamigen Abschnitt 7.4 erklärt.
Der Aufruf einer Methode mit generischen Typparametern ist sehr einfach. Sie instanziieren in gewohnter Weise zuerst die Klasse und rufen die Methode unter Angabe des gewünschten konkreten Datentyps auf:
    GenericClass<string> obj = new GenericClass<string>();
    obj.GenericMethod<int>(25);
Sie können sogar auf die Typangabe verzichten, denn auch in diesem Fall wird der C#-Compiler die richtige Schlussfolgerung ziehen:
    obj.GenericMethod(25);
Dieser Aufruf ist absolut gleichwertig. #### Methoden und Constraints Muss der generische Typparameter einer Methode bestimmten Bedingungen genügen, legen Sie einen Constraint fest. Die Syntax entspricht der der Constraints einer Klasse. Allerdings ist es nicht möglich, einen Constraint für einen generischen Typparameter einer Methode zu definieren, der bereits auf Klassenebene festgelegt ist.
    public void GenericMethod<T>(T sender) where T: IComparable
    {
      ...
    }
#### Gleichnamige generische Typparameter Dass Felder auf Klassenebene und Methodenparameter gleichnamig sein dürfen, ist Ihnen bekannt. Diese Freizügigkeit haben Sie mit generischen Typparametern nicht: Ein Platzhalter, der auf Klassenebene angegeben ist, darf für eine Methode nicht mehr verwendet werden, da der C#-Compiler nicht in der Lage ist, diese Doppeldeutigkeit aufzulösen.
    class GenericClass<T> {
      // fehlerhafter Typparameter
      public T GenericMethod<T>(T obj) { ... }
    }
#### Statische generische Methoden Generische Typparameter und Constraints können sowohl für Instanzmethoden als auch für statische Methoden festgelegt werden, zum Beispiel:
    class GenericClass<T> {
      public static void GenericMethod<K>(T obj1, K obj2) {...}
    }
Der Aufruf erfolgt mit:
    GenericClass<string>.GenericMethod<int>("Hallo", 44);
oder in verkürzter Form mit:
    GenericClass<string>.GenericMethod("Hallo", 44);
### 7.2.4 Generics und Vererbung Generische Klassen können abgeleitet werden. Die Regeln ähneln denen, die Sie schon kennen. Aufgrund der besonderen Natur generischer Klassen sind dabei jedoch ein paar Besonderheiten zu beachten. Ist die Basisklasse generisch, kann die abgeleitete Klasse den generischen Typparameter übernehmen und selbst generisch sein.
    class BaseClass<T> {...}
    class SubClass<T> : BaseClass<T> {...}
Die Basisklasse könnte die konkreten Datentypen durch einen Constraint auf ganz bestimmte Typen eingrenzen. Diese gilt dann auch für die abgeleitete Klasse und muss hinter der Basisklasse angegeben werden.
    class BaseClass<T> where T : IComparable {...}
    class SubClass<T> : BaseClass<T> where T : IComparable {...}
Soll die abgeleitete Klasse nicht generisch sein, muss der Typparameter in der Angabe der Basisklasse durch einen konkreten Datentyp ersetzt werden, wie hier gezeigt ist:
    class BaseClass<T> {...}
    class SubClass : BaseClass<int> {...}
Sie können umgekehrt auch dann eine generische Subklasse entwickeln, wenn die Basisklasse nicht generisch ist. Sind in der Basisklasse virtuelle Methoden definiert, wird es noch einmal spannend, denn die Methode könnte in der Basisklasse einen generischen Typparameter haben. Virtuelle Methoden können mit override überschrieben werden. Ob der generische Typparameter durch einen konkreten Datentyp ersetzt werden muss oder ob der Typparameter auch in der überschreibenden Methode angeführt werden darf, entscheidet sich schon bei der Festlegung der Subklasse. Spielen wir den Fall durch, dass die ableitende Klasse den geerbten generischen Typparameter konkret ersetzt, also:
    class BaseClass<T> {
      public virtual T MyMethod() {...}
    }
    class SubClass : BaseClass<int> {
      public override int MyMethod() {...}
    }
Wie weiter oben beschrieben wurde, muss der Typparameter durch eine konkrete Angabe ersetzt werden. Das verpflichtet auch dazu, den gewünschten Datentyp in der Signatur der überschreibenden Methode zu benennen. Dass sich die Methode polymorph verhalten wird, bedarf kaum noch einer Erwähnung. Soll auch die abgeleitete Klasse generisch sein, muss die virtuelle Methode mit generischen Typparametern überschrieben werden.
    class BaseClass<T> {
      public virtual T MyMethod() {...}
    }
    class  SubClass<T> : BaseClass<T> {
      public override T MyMethod() {...}
    }
### 7.2.5 Konvertierung von Generics Die implizite Konvertierung eines generischen Typparameters ist nur statthaft, wenn der Zieldatentyp Object ist oder einer der Typen, die als Constraint hinter where angeführt sind.
    class DemoB<T> where T : DemoA, IComparable {
      public void MyMethod(T obj) {
        IComparable var1 = obj;
        DemoA var2 = obj;
        Object var3 = obj;
      }
    }
Die Klasse DemoB beschreibt den Typparameter T, der den folgenden Bedingungen genügen muss: Der konkrete Typ muss von der Klasse DemoA abgeleitet sein und das Interface IComparable implementieren. Die Zuweisungen in MyMethod sind damit gültig und typsicher. An DemoB wollen wir nun noch eine Manipulation vornehmen, indem wir auf die Constraints verzichten. Wir haben dann immer noch die Möglichkeit, implizit in Object zu casten; die Konvertierung in eine Schnittstelle muss jedoch explizit erfolgen. Weil der Compiler zur Kompilierzeit nicht weiß, durch welchen konkreten Typ der Typparameter zur Laufzeit ersetzt wird, wird er diesen Cast akzeptieren. Nicht erlaubt ist hingegen die explizite Konvertierung in irgendeine Klasse.
    DemoB<T> {
      public void MyMethod(T obj) {
    // korrekt:
        IComparable var1 = (IComparable)obj;
    // fehlerhaft:
        DemoA var2 = (DemoA)obj;
        object var3 = obj;
      }
    }
Es bleibt festzustellen, dass das explizite Umwandeln von Typen nicht ganz ungefährlich ist und zur Laufzeit eine Ausnahme verursachen kann, wenn der generische Typ nicht die Schnittstelle implementiert. Um dieser Gefahrenquelle aus dem Weg zu gehen, bietet sich eine Alternative mit den beiden Operatoren is und as an. Zur Erinnerung: Mit beiden lässt sich der Typ einer Referenz überprüfen. is liefert true zurück, wenn der linke Operand vom Typ des rechten ist. Der as-Operator führt in diesem Fall sogar eine Konvertierung durch, andernfalls ist der Rückgabewert null. Das folgende Codefragment zeigt, wie Sie die genannten Operatoren zur Typüberprüfung einsetzen können.
    class DemoB<T> {
      public void MyMethod(T obj) {
        if (obj is string) {...}
        if (obj is IComparable) {...}
        // alternativ:
        int intVar = obj as string;
        if (intVar != null) {...}
        IComparable temp = obj as IComparable;
        if (temp != null) {...}
      }
    }
### 7.2.6 Generische Delegates Delegates können außerhalb des Gültigkeitsbereichs einer Klasse oder in einer Klasse selbst definiert werden. Das gilt auch für Delegates, die einen generischen Typparameter beschreiben. Generische Delegates erweisen sich als besonders nützlich, wenn mehrere ähnliche Events ausgelöst werden. Ein kleiner Satz generischer Delegates, die sich in der Anzahl und dem Typ der Parameter unterscheiden, reicht oftmals vollkommen aus, um alle Ereignishandler bedienen zu können. Sehen wir uns ein generisches Delegate an:
    public delegate void MyDelegate<T>(T obj);
Auch hier gibt T den Parametertyp vor. Instanziiert wird ein generisches Delegate in der gleichen Weise wie jedes andere, also entweder mit:
    MyDelegate<int> del = new MyDelegate<int>(MyEventHandler);
oder einfach nur mit:
    MyDelegate<int> del = MyEventHandler;
Die Definition eines generischen Delegates erlaubt es uns außerdem, den konkreten Typ mit where zu beschränken. Wollen Sie beispielsweise den Typparameter T des Delegates MyDelegate auf die Typen begrenzen, die von der Klasse ClassA abgeleitet sind und die Schnittstelle IMyInterface implementieren, würde die Anweisung wie folgt lauten:
    public delegate void MyDelegate<T>(T obj) where T : ClassA, IMyInterface;
### 7.2.7 Generische Klassen in der .NET-Klassenbibliothek Im Namespace System.Collections.Generic finden Sie zahlreiche generische Auflistungen und Interfaces, unter anderem auch eine Klasse Stack<T>, ähnlich der, die wir zu Anfang unserer Ausführungen beschrieben haben. In Tabelle 7.2 sind die wichtigsten Klassen und Schnittstellen nebst den nichtgenerischen Implementierungen aufgeführt.

Tabelle 7.2 Generische Klassen und Schnittstellen und ihre Pendants

System.Collections.Generic | System.Collections
Collection<T> | CollectionBase
Dictionary<K, V> | Hashtable
IComparer<T> | IComparer
IComparable<T> | IComparable
IEnumerable<T> | IEnumerable
IList<T> | IList
List<T> | ArrayList
Queue<T> | Queue
SortedDictionary<K, V> | SortedList
Stack<T> | Stack
Hinweis
Mit Collections (Auflistungen) werden wir uns in Kapitel 8, »Auflistungsklassen (Collections)«, noch näher beschäftigen. Dort werden auch Beispiele gezeigt, die auf den Generics basieren.