## 8.6 Generische Auflistungsklassen Ein ganz wesentlicher Nachteil der Auflistungen aus dem Namespace System.Collections ist, dass sie allgemein gehalten sind und immer den Typ Object verwalten. Damit können Sie in einem Auflistungsobjekt alles speichern, von einem Integer über einen Stream bis hin zur Datenbanktabelle. Wenn Sie nicht wissen, welche Typen von der Auflistung verwaltet werden, haben Sie praktisch keine Chance, die Elemente auszuwerten. Bedenken Sie, dass Sie jedes Element der Auflistung zuerst in den richtigen Typ konvertieren müssen, um dessen spezifische Eigenschaften nutzen zu können. Generische Auflistungsklassen haben diesen Nachteil nicht. Sie sind auf einen bestimmten Typ gedrillt. Das bedeutet, dass Sie vorschreiben, welche Typen von der Collection verwaltet werden sollen. Der Code wird einfacher und besser lesbar, weil Sie meistens keine Konvertierung vornehmen müssen, um auf die spezifischen Member der verwalteten Elemente zuzugreifen. Im Wesentlichen habe ich Ihnen in diesem Kapitel zwei nichtgenerische Auflistungsklassen vorgestellt: die indexbasierte ArrayList und das Dictionary Hashtable. Beide haben einen generischen Gegenspieler in List<T> und Dictionary<TKey, TValue>. In diesem Abschnitt werde ich Ihnen List<T> vorstellen. Anhand des hier Gesagten erübrigt sich eine weitere Vertiefung der Klasse Dictionary<TKey, TValue>. ### 8.6.1 Die Interfaces der generischen Auflistungsklassen Sie wissen, dass alle nichtgenerischen Auflistungsklassen die beiden Schnittstellen ICollection und IEnumerable implementieren. Zudem implementieren die indexbasierten Auflistungen die Schnittstelle IList und alle Schlüssel-Wert-Paar-Auflistungen die Schnittstelle IDictionary. Haben wir es mit generischen Auflistungsklassen zu tun, sind auch diese Schnittstellen generisch. Somit handelt es sich um IEnumerable<T> ICollection<T> IDictionary<TKey, TValue> IList<T> Eine genauere Beschreibung dieser Interfaces ist nicht notwendig, da sie sich in ihrem elementaren Verhalten nicht von den nichtgenerischen unterscheiden. ### 8.6.2 Die generische Auflistungsklasse »List<T>« Im Großen und Ganzen ist kein wesentlicher Unterschied zwischen den Methoden und Eigenschaften einer ArrayList und List<T> festzustellen. Sie fügen mit Add oder AddRange Objekte hinzu, Sie löschen Auflistungselemente mit Remove oder RemoveAt, Sie besorgen sich den Index eines Elements mit IndexOf. Natürlich sind alle Methoden generisch geprägt, also auf den Typ hin spezialisiert, den Sie bei der Instanziierung der Klasse angegeben haben. Konzentrieren wir uns daher sofort auf ein Beispielprogramm. #### Sortieren einer indexbasierten Collection In Abschnitt 8.3.3 habe ich Ihnen im Beispielprogramm IComparerSample den Einsatz von Vergleichsklassen vorgestellt, um die Elemente einer ArrayList nach eigenen Kriterien zu sortieren. Ausgangspunkt war die Klasse Person mit den beiden Feldern Name und Wohnort. In der Anwendung wurden mehrere Person-Objekte einem ArrayList-Objekt hinzugefügt und konnten entweder nach Wohnort oder Name sortiert an der Konsole ausgegeben werden. Vielleicht erinnern Sie sich noch, dass eine verhältnismäßig komplexe Konvertierung notwendig war, um den Rückgabewert des Vergleichs zu bilden. In der Vergleichsklasse NameComparer sah die Anweisung wie folgt aus:
    return ((Person)x).Name.CompareTo(((Person)y).Name);
Mit der generischen Auflistungsklasse IList<T> geht alles viel einfacher. Person-Objekte werden nun in einem Objekt von diesem Typ direkt verwaltet:
    List<Person> liste = new List<Person>();
Damit gilt die Collection als streng typisiert: Sie verwaltet nur Objekte vom Typ Person und wird strikt andere Typen abweisen. Unser Ziel sei es auch in diesem Beispiel, entweder nach Name oder Wohnort sortieren zu können. So wie die Klasse ArrayList bietet auch List<T> eine Methode Sort an, die das gewährleistet. Schauen wir uns deren Definition an:
    public void Sort(IComparer<T> comparer)
Im Gegensatz zur Methode Sort der ArrayList wird nun ein Objekt erwartet, das eine generische Schnittstelle implementiert. Sehen wir uns doch auch noch die Definition der Schnittstellenmethode an:
    public interface IComparer<T> {
      int Compare(T x, T y);
    }
Compare vergleicht anscheinend zwei Objekte miteinander, deren Typ durch den Typparameter T beschrieben wird. Bezogen auf unser Beispiel bedeutet das, den Typparameter T durch Person zu ersetzen. Damit werden die Referenzen, die der Methode Compare als Argumente übergeben werden, vor deren Eintritt in den Code der Methode gefiltert. Diese Erkenntnis wollen wir nun auch umsetzen. Dazu sind die beiden Klassen NameComparer und WohnortComparer im Vergleich zur ersten Version des Beispielprogramms ein wenig zu ändern – besser gesagt, zu vereinfachen. Anhand der Klasse WohnortComparer sei dies gezeigt.
    // Vergleichsklasse - Kriterium 'Wohnort'
    class WohnortComparer : IComparer<Person> {
      public int Compare(Person x, Person y) {
        // prüfen auf null-Übergabe
        if (x == null && y == null) return 0;
        if (x == null) return 1;
        if (y == null) return -1;
        // Vergleich
        return x.Wohnort.CompareTo(y.Wohnort);
      }
    }
Auch wenn der Umgang mit generischen Klassen und Schnittstellen im ersten Moment ein wenig gewöhnungsbedürftig erscheint, ist der Vergleich zweier Objekte deutlich einfacher geworden, weil keine Konvertierung mehr notwendig ist. Das ist darauf zurückzuführen, dass wir durch Einsatz der generischen Schnittstellenmethode garantieren können, ausschließlich Person-Objekte zu vergleichen. Zudem können wir uns auch die Typüberprüfung sparen, die in der Ursprungsversion unseres Beispiels noch notwendig war. Zum Schluss folgt hier der gesamte Code des Beispiels:
    // -----------------------------------------------------------
    // Beispiel: ...\Kapitel 8\GenericListSample
    // -----------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        List<Person> liste = new List<Person>();
        // generische Liste füllen
        Person pers1 = new Person("Meier", "Berlin");
        liste.Add(pers1);
        Person pers2 = new Person("Arnhold", "Köln");
        liste.Add(pers2);
        Person pers3 = new Person("Graubär", "Aachen");
        liste.Add(pers3);
        // nach Wohnorten sortieren
        liste.Sort(new WohnortComparer());
        Console.WriteLine("Liste nach Wohnorten sortiert");
        ShowSortedList(liste);
        // nach Namen sortieren
        liste.Sort(new NameComparer());
        Console.WriteLine("Liste nach Namen sortiert");
        ShowSortedList(liste);
        Console.ReadLine();
      }
      static void ShowSortedList(IList<Person> liste) {
        foreach (Person temp in liste) {
          Console.Write("Name = {0,-12}", temp.Name);
          Console.WriteLine("Wohnort = {0}", temp.Wohnort);
        }
        Console.WriteLine();
      }
    }
    // Vergleichsklasse - Kriterium 'Wohnort'
    class WohnortComparer : IComparer<Person> {
      public int Compare(Person x, Person y) {
        // prüfen auf null-Übergabe
        if (x == null && y == null) return 0;
        if (x == null) return 1;
        if (y == null) return -1;
        // Vergleich
        return x.Wohnort.CompareTo(y.Wohnort);
      }
    }
    // Vergleichsklasse - Kriterium 'Name'
    class NameComparer : IComparer<Person> {
      public int Compare(Person x, Person y) {
        // prüfen auf null-Übergabe
        if (x == null && y == null) return 0;
        if (x == null) return 1;
        if (y == null) return -1;
        // Vergleich
        return x.Name.CompareTo(y.Name);
      }
    }
    class Person {
      public string Name;
      public string Wohnort;
      public Person(string name, string ort) {
        Name = name;
        Wohnort = ort;
      }
    }
#### Vergleiche mithilfe des Delegates »Comparison<T>« Im Vergleich zur Klasse ArrayList hat die Klasse List<T> noch eine weitere interessante Überladung der Methode Sort, die es uns gestattet, den Code noch kürzer und intuitiver zu schreiben. Sehen wir uns die Definition der Methode an.
    public void Sort(Comparison<T> comparison)
Die Überladung erwartet die Übergabe eines Comparison<T>-Objekts. Dabei handelt es sich um ein Delegate, das auf die Methode zeigt, die den Vergleich zweier T-Objekte durchführt und das Resultat des Vergleichs an den Aufrufer liefert. Das Delegate ist wie folgt definiert:
    public delegate int Comparison<T>(T x, T y);
Bezogen auf das Beispielprogramm GenericListSample können wir auf die beiden Vergleichsklassen verzichten. Wir stellen stattdessen zwei Methoden bereit, die dasselbe zu leisten vermögen. Das folgende Beispielprogramm zeigt, wie Sie diese Überladung von Sort einsetzen können.
    // -----------------------------------------------------------
    // Beispiel: ...\Kapitel 8\GenericListSample
    // -----------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        List<Person> arrList = new List<Person>();
        ...
        // nach Wohnorten sortieren
        arrList.Sort(CompareByWohnort);
        Console.WriteLine("Liste nach Wohnorten sortiert");
        ShowSortedList(arrList);
        // nach Namen sortieren
        arrList.Sort(CompareByName);
        Console.WriteLine("Liste nach Namen sortiert");
        ShowSortedList(arrList);
        Console.ReadLine();
      }
      public static int CompareByName(Person x, Person y) {
        // prüfen auf null-Übergabe
        if (x == null && y == null) return 0;
        if (x == null) return 1;
        if (y == null) return -1;
        // Vergleich
        return x.Name.CompareTo(y.Name);
      }
      public static int CompareByWohnort(Person x, Person y) {
        // prüfen auf null-Übergabe
        if (x == null && y == null) return 0;
        if (x == null) return 1;
        if (y == null) return -1;
        // Vergleich
        return x.Wohnort.CompareTo(y.Wohnort);
      }
      static void ShowSortedList(IList<Person> liste) { ... }
    }
#### Verschachtelte generische Klassen Nun kommen wir zum zweiten neu aufgelegten Beispiel. Es handelt sich hierbei um die (Fußball-)Mannschaft, die als Collection ausgebildet ist und Objekte vom Typ Spieler verwalten soll. Das Beispiel hatten wir in Abschnitt 7.4, »Indexer«, es soll aber nun um eine weitere Anforderung ergänzt werden. Dazu sollen mehrere Mannschaften eine gemeinsame Liga bilden, die ebenfalls eine Collection ist, ihrerseits aber nur Mannschaften verwaltet. Zur Lösung der Aufgabenstellung können wir auch in diesem Beispiel auf die Klasse List<T> zurückgreifen.
    // -----------------------------------------------------------
    // Beispiel: ...\Kapitel 8\Bundesliga
    // -----------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        // 1. Mannschaft
        Mannschaft s04 = new Mannschaft("Schalke 04");
        s04.Add(new Spieler("Müller", 34, SpielPosition.Stürmer));
        s04.Add(new Spieler("Fischer", 23,SpielPosition.Mittelfeldspieler));
        s04.Add(new Spieler("Meier", 29, SpielPosition.Torwart));
        // 2. Mannschaft
        Mannschaft werder = new Mannschaft("Werder Bremen");
        werder.Add(new Spieler("Graf", 37, SpielPosition.Mittelfeldspieler));
        werder.Add(new Spieler("Weide", 31, SpielPosition.Torwart));
        werder.Add(new Spieler("Maradona", 28, SpielPosition.Stürmer));
        // Bundesliga definieren
        List<List<Spieler>> Bundesliga = new List<List<Spieler>>();
        Bundesliga.Add(s04);
        Bundesliga.Add(werder);
        // Ausgabe aller Mannschaften und deren Spieler
        foreach (Mannschaft tempTeam in Bundesliga) {
          Console.WriteLine(tempTeam.Vereinsname);
          foreach (Spieler spieler in tempTeam) {
            Console.WriteLine("{0,-10}{1,-3}{2}",
                    spieler.Name, spieler.Alter, spieler.Position);
          }
          Console.WriteLine();
        }
        Console.ReadLine();
      }
    }
    // ------ Spieler --------
    class Spieler {
      public string Name;
      public int Alter;
      public SpielPosition Position;
      public Spieler(string name, int alter, SpielPosition position) {
        Name = name;
        Alter = alter;
        this.Position = position;
      }
    }
    // ------ Mannschaft ---------
    class Mannschaft : List<Spieler> {
      public string Vereinsname;
      public Mannschaft(string verein) {
        Vereinsname = verein;
      }
    }
    // ----- diverse Positionen -----
    enum SpielPosition {
      Torwart,
      Verteidiger,
      Mittelfeldspieler,
      Stürmer
    }
Da eine Mannschaft nur aus Spielern besteht und eine Liga ihrerseits nur aus Mannschaften, drängt sich der Typ List<T> auch in diesem Beispiel geradezu auf. Im Fall der Mannschaft ist der Typparameter dann natürlich Spieler, eine Klasse, die in der Anwendung definiert ist. Um eine Mannschaft zu erzeugen, würde es durchaus genügen, mit
    List<Spieler> Schalke04 = new List<Spieler>();
ein Objekt zu erzeugen. Diese Mannschaft hätte dann jedoch keine spezifischen Eigenschaften, und der Vereinsname soll doch mindestens vorhanden sein, oder? Daher ist eine Klasse Mannschaft definiert, die von List<Spieler> abgeleitet ist und in einem Feld den Vereinsnamen als Zeichenfolge speichern kann. Die Bundesliga ist als verschachtelter generischer Typ ausgebildet. Es handelt sich dabei um eine Auflistung, die selbst nur Mannschaften, also Objekte vom Typ List<Spieler>, verwalten kann.
    List<List<Spieler>> Bundesliga = new List<List<Spieler>>();
Eine solche Verschachtelung ist möglich, weil List<Spieler> ganz präzise einen bestimmten Typ definiert, der selbst den generischen Typparameter der Klasse List<T> ersetzt. Solche Verschachtelungen können Sie in praktisch beliebiger Tiefe codieren. Der Collection-Initialisierer, den Sie weiter oben im Zusammenhang mit den nichtgenerischen Auflistungen bereits kennengelernt haben, kann auch von einer generischen Collection genutzt werden. So hätten wir auch die erste Mannschaft folgendermaßen initialisieren können:
    Mannschaft s04 = new Mannschaft("Schalke 04"){
      new Spieler("Müller", 34, SpielPosition.Stürmer),
      new Spieler("Fischer", 23, SpielPosition.Mittelfeldspieler),
      new Spieler("Meier", 29, SpielPosition.Torwart)
    };