Programmieren in C#: Einführung


Kapitel 7: Generika


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


7.1 Allgemeines

Wiederverwendung auf die Spitze getrieben

Das .NET-Framework unterstützt seit der Version 2.0 die generische Programmierung. Da diese direkt vom .NET-Framework unterstützt wird, handelt es sich somit nicht um ein Sprachmerkmal der Programmiersprache C#. So kann auch in anderen .NET-Programmiersprachen generisch programmiert werden.

Auch außerhalb der .NET-Welt ist die generische Programmierung nicht unbekannt. Es handelt sich also nicht um eine Erfindung Microsofts, sondern um ein altbekanntes Programmierparadigma.

Sinn und Zweck der generischen Programmierung ist es, die Wiederverwendbarkeit von Quellcode auf die Spitze zu treiben. Während Sie bereits gesehen haben, wie Sie Klassen, die zum Beispiel im .NET-Framework zur Verfügung gestellt werden, einsetzen und somit wiederverwenden können, ermöglicht die generische Programmierung, Code zu entwickeln, der keine bestimmte Datentypen voraussetzt. Weil derartiger Code mit beliebigen Datentypen verwendet werden kann, muss er nur einmal entwickelt werden - egal, welche Datentypen verarbeitet werden sollen.

Die generische Programmierung ist somit ein Hilfsmittel, das zur Arbeitserleichterung des Entwicklers beitragen will. Da viele Klassen im .NET-Framework die generische Programmierung unterstützen, sollten Sie sich mit ihr auch deswegen vertraut machen, um das .NET-Framework in all seinen Bestandteilen verstehen zu können.


7.2 Generische Klassen

Auflistungen als typische Vertreter generischer Klassen

Der in den vorherigen Kapiteln entwickelte Favoriten-Manager verwendet ein Array, um Favoriten zu speichern. Weil ein Array eine konstante Größe hat, muss bei jedem Aufruf von Add() ein neues Array erstellt werden, das einen zusätzlichen Speicherplatz bietet, um den neuen Favoriten speichern zu können. Damit die bisher gespeicherten Favoriten nicht verloren gehen, müssen sie vom alten in das neue Array kopiert werden.

Während der Code einwandfrei funktioniert, kann er vereinfacht werden. Im Namensraum System.Collections bietet das .NET-Framework zahlreiche Klassen an, mit denen sogenannte Auflistungen von Objekten einfach verwaltet werden können. So existiert zum Beispiel eine Klasse ArrayList, die grundsätzlich genauso funktioniert wie ein Array - mit dem entscheidenden Unterschied, dass die Größe der Klasse ArrayList nicht konstant ist.

using System; 
using System.Collections; 

public class FavoriteManager 
{ 
  ArrayList favs = new ArrayList(); 

  public void Add(string name, string url) 
  { 
    favs.Add(new Favorite(name, new Uri(url))); 
  } 

  public Favorite[] Favorites 
  { 
    set 
    { 
      favs.Clear(); 
      favs.AddRange(value); 
    } 

    get { return (Favorite[])favs.ToArray(); } 
  } 
} 

Der Favoriten-Manager speichert nun Favoriten, die per Add() hinzugefügt werden, in einem Objekt vom Typ ArrayList. Da diese Klasse unter anderem eine Methode Add() anbietet, kann dem Objekt jederzeit ein neuer Favorit hinzugefügt werden. ArrayList verwaltet den notwendigen Speicherplatz automatisch.

Auch in den set- und get-Accessoren der Eigenschaft Favorites wird auf das neue Objekt zugegriffen: Während mit Clear() momentan gespeicherte Favoriten entfernt werden, um anschließend mit AddRange() ein Array mit neuen Favoriten zu speichern, werden mit ToArray() alle gespeicherten Favoriten als herkömliches Array zurückgegeben.

Während die Klasse ArrayList die Implementation des Favoriten-Managers vereinfacht, tritt jedoch ein neues Problem auf: Während favs vorher ein Array war, das Objekte vom Typ Favorite gespeichert hat, speichert die Klasse ArrayList Objekte vom Typ Object. Aufgrund der Klassenhierarchie in .NET sind alle Objekte vom Typ Favorite gleichzeitig vom Typ Object. Sie können deswegen ohne Probleme in einem ArrayList-Objekt gespeichert werden. Weil ArrayList intern aber nur Objekte vom Typ Object speichert, werden bei einem Zugriff auch nur Objekte vom Typ Object zurückgegeben. Das ist der Grund, warum der Rückgabewert von ToArray() in ein Array vom Typ Favorite gecastet werden muss.

Grundsätzlich sollte so selten wie möglich gecastet werden. Denn durch ein Casting teilen Sie dem Compiler mit, dass der Datentyp eines Objekts ein anderer ist als der, den der Compiler sieht. Der Compiler kann dabei nicht mehr tun als darauf zu vertrauen, dass Sie Recht haben. Geben Sie nämlich einen Datentyp an, zu dem ein Objekt nicht gecastet werden kann, stürzt das Programm ab und es erscheint eine Fehlermeldung.

Seit dem .NET-Framework 2.0 existiert ein Namensraum System.Collections.Generic, in dem sich generische Auflistungen befinden. So steht dort eine generische Klasse List zur Verfügung, die grundsätzlich genauso funktioniert wie ArrayList. Der Unterschied ist, dass List jeweils genau die Objekte speichert, die Sie angeben, wenn Sie diese Klasse verwenden - und nicht wie ArrayList Objekte vom Typ Object.

using System; 
using System.Collections.Generic; 

public class FavoriteManager 
{ 
  List<Favorite> favs = new List<Favorite>(); 

  public void Add(string name, string url) 
  { 
    favs.Add(new Favorite(name, new Uri(url))); 
  } 

  public Favorite[] Favorites 
  { 
    set 
    { 
      favs.Clear(); 
      favs.AddRange(value); 
    } 

    get { return favs.ToArray(); } 
  } 
} 

Die Klasse FavoriteManager verwendet nun die generische Auflistung List. Weil diese Klasse generisch ist, muss in spitzen Klammern hinter dem Klassennamen angegeben werden, welche Objekte jeweils in der Auflistung gespeichert werden sollen. So wird im obigen Beispielcode angegeben, dass über die Referenzvariable favs auf eine Liste zugegriffen wird, die Objekte vom Typ Favorite speichert.

Wenn Sie sich ansehen, wie innerhalb des get-Accessors auf die Eigenschaft Favorites zugegriffen wird, stellen Sie fest, dass ein Casting nicht mehr notwendig ist. Weil der generischen Liste vom Typ List angegeben wurde, dass sie Objekte vom Typ Favorite speichert, gibt ToArray() ein Array vom Typ Favorite zurück - und nicht ein Array vom Typ Object, wie es ToArray() von ArrayList macht.

Neben List stellt das .NET-Framework im Namensraum System.Collections.Generic weitere generische Auflistungen zur Verfügung, die die seit der ersten Version des .NET-Frameworks im Namensraum System.Collections definierten Auflistungen ersetzen. In der Dokumentation des Namensraums System.Collections.Generic finden Sie eine Übersicht über alle generischen Auflistungen. Sie sollten diese Klassen grundsätzlich den nicht-generischen Klassen aus dem Namensraum System.Collections vorziehen.

Selbstverständlich können Sie nicht nur generische Klassen einsetzen, sondern auch selbst erstellen. So soll im Folgenden der Favoriten-Manager zu einer generischen Klasse umgebaut werden. Entwickler, die den Favoriten-Manager einsetzen, sollen selbst angeben dürfen, welche Klasse sie für Favoriten verwenden wollen. Damit soll die Kopplung des Favoriten-Managers an die momentan verwendete Klasse Favorite aufgehoben werden.

using System; 
using System.Collections.Generic; 

public class FavoriteManager<TFavorite> 
  where TFavorite : Favorite, new() 
{ 
  List<TFavorite> favs = new List<TFavorite>(); 

  public void Add(string name, string url) 
  { 
    TFavorite fav = new TFavorite(); 
    fav.Name = name; 
    fav.URL = new Uri(url); 
    favs.Add(fav); 
  } 

  public TFavorite[] Favorites 
  { 
    set 
    { 
      favs.Clear(); 
      favs.AddRange(value); 
    } 

    get { return favs.ToArray(); } 
  } 
} 

Um die Klasse FavoriteManager generisch zu machen, müssen lediglich hinter dem Klassennamen in spitzen Klammern Parameter angegeben werden. Im obigen Beispielcode hat die generische Klasse FavoriteManager genau einen Parameter, der TFavorite heißt. Gemäß den Richtlinien zur generischen Programmierung im .NET-Framework werden Parameter mit einem großen T geschrieben.

Entwickler, die die generische Klasse FavoriteManager einsetzen möchten, müssen hinter dem Klassennamen einen Datentyp für den Favoriten angeben. Welchen Datentyp Entwickler auch immer angeben, er wird überall da in der Klasse verwendet, wo der Parameter TFavorite angegeben ist. So wird dieser Parameter zum Beispiel in spitzen Klammern an List übergegeben. Außerdem hat sich der Datentyp der Eigenschaft Favorites geändert und heißt nun TFavorite[] statt Favorite[].

Ausschließlich einen Parameter in spitzen Klammern hinter einem Klassennamen anzugeben reicht oft nicht aus, um eine Klasse generisch zu machen. Immer dann, wenn auf Eigenschaften oder Methoden des entsprechenden Parameters zugegriffen wird, muss schließlich sichergestellt sein, dass Entwickler, die die generische Klasse verwenden, als Parameter einen Datentyp angeben, der tatsächlich die benötigten Eigenschaften und Methoden anbietet.

So muss der Favoriten-Manager zum Beispiel in der Methode Add() den entsprechenden Datentyp des Favoriten mit new instantiieren. Das heißt, der Datentyp muss einen öffentlichen Konstruktor besitzen. Außerdem verlangt C#, dass es sich hierbei um den Standardkonstruktor handeln muss. Das ist der Grund, warum im obigen Beispielcode keine Parameter an den Konstruktor übergeben werden.

Anforderungen an Datentypen, die als Parameter für generische Klassen verwendet werden, werden als Einschränkungen hinter dem Klassenkopf und einem Schlüsselwort where angegeben. Hinter where wird der Name des entsprechenden Parameters gesetzt, um hinter einem Doppelpunkt anzugeben, welche Anforderungen zu erfüllen sind. Wenn diese Anforderungsliste ein new() enthält, bedeutet dies, dass Datentypen einen öffentlichen Standardkonstruktor anbieten müssen, weil sie in der Klasse mit new instantiiert werden müssen. Genau dies ist im obigen Beispielcode der Fall.

Die Anforderungsliste für TFavorite enthält nicht nur ein new(), sondern auch die Klasse Favorite. Dies bedeutet, dass Datentypen nicht nur einen öffentlichen Standardkonstruktor anbieten müssen, sondern außerdem identisch mit Favorite oder von dieser Klasse abgeleitet sein müssen. Der Grund für diese Anforderung ist, dass in Add() nach der Instantiierung des Datentyps der Name und die Adresse über die Eigenschaften Name und URL gespeichert werden. Dies funktioniert natürlich nur, wenn der entsprechende Datentyp diese beiden Eigenschaften besitzt. Das ist sichergestellt, wenn es sich beim Datentypen um die Klasse Favorite oder um eine Kindklasse handelt. Denn Favorite definiert die beiden Eigenschaften Name und URL.

Wenn Sie generische Klassen entwickeln, müssen Sie also nicht nur Datentypen durch Parameter ersetzen. Sie müssen außerdem häufig hinter where Beschränkungen für die Parameter definieren. Wenn im obigen Beispielcode keine Beschränkungen angegeben würden, würde sich der C#-Compiler weigern, den Code zu kompilieren. Eine generische Klasse ohne Beschränkungen kann nämlich mit jedem beliebigen Datentyp verwendet werden. Genau das funktioniert aber mit dem Favoriten-Manager nicht, denn der entsprechende Datentyp muss immerhin instantiiert werden können und die Eigenschaften Name und URL anbieten.


7.3 Generische Interfaces

Verbesserte zielgenaue Interfaces

Das .NET-Framework kennt nicht nur generische Klassen, sondern auch generische Interfaces. So wie grundsätzlich generische Auflistungen den nicht-generischen Auflistungen vorzuziehen sind, sollten Sie auch bevorzugt auf generische Interfaces zugreifen.

Im Abschnitt 4.6, „Interfaces“ wurde ein Interface IComparable verwendet. Dieses Interface verlangt von Klassen, die es implementieren, eine Methode CompareTo() bereitzustellen. Während diese Methode aufgerufen werden kann, um zwei Objekte der gleichen Klasse zu vergleichen, erwartet die Methode einen Parameter vom Typ Object - und nicht einen Parameter vom Typ der entsprechenden Klasse.

Nachdem im .NET-Framework seit der Version 2.0 die generische Programmierung unterstützt wird, existiert das Interface IComparable auch in einer generischen Version. Diese soll im Folgenden verwendet werden.

using System; 
using System.Diagnostics; 

class Favorite2 : Favorite, IComparable<Favorite2> 
{ 
  DateTime dateTime; 
  int i; 

  public Favorite2(string name, Uri url, int i) 
    : base(name, url) 
  { 
    this.i = i; 
    dateTime = DateTime.Now; 
  } 

  public override void Open() 
  { 
    ++i; 
    Process.Start(url.ToString()); 
  } 

  public int CompareTo(Favorite2 fav) 
  { 
    if (fav.i > i) 
      return 1; 
    else if (fav.i < i) 
      return -1; 
    else 
      return 0; 
  } 
} 

Generische Interfaces werden wie generische Klassen verwendet: Es müssen genauso viele konkrete Datentypen wie Parameter in spitzen Klammern hinter dem Interfacenamen angegeben werden. Da IComparable nur einen Parameter erwartet, wird im obigen Beispielcode als einziger Parameter Favorite2 angegeben.

Weil das generische Interface IComparable verwendet wird, ist der Datentyp des Parameters von CompareTo() nicht mehr Object, sondern genau der, der als Parameter IComparable angegeben wurde - also Favorite2. Somit ist es nicht mehr notwendig, den Parameter zu casten.


7.4 Generische Ereignisverarbeitung

Ereignisse verarbeiten ohne Castings

Es gibt nicht nur generische Klassen und Interfaces, sondern auch generische Delegates. Da Delegates wie im Kapitel 6, Ereignisse erfahren in der Ereignisverarbeitung verwendet werden, kann diese ebenfalls durch Generika vereinfacht werden.

So bietet zum Beispiel das .NET-Framework seit der Version 2.0 den Delegate EventHandler auch in einer generischen Form an:

public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e) where TEventArgs : EventArgs 

Der generische Delegate EventHandler erwartet als Parameter einen Datentyp, der laut den Beschränkungen hinter where entweder EventArgs oder eine Kindklasse sein muss. Entwickler, die zusätzliche Daten im zweiten Parameter unterbringen müssen, können demnach eine Kindklasse von EventArgs erstellen und dann mit dieser Klasse den Delegate EventHandler wiederverwenden.

Während es der generische Delegate EventHandler ermöglicht, einen anderen Datentyp als EventArgs für den zweiten Parameter zu verwenden, hat der erste Parameter immer noch den Datentyp Object. Wenn ein Ereignis immer nur von Objekten eines bestimmten Datentyps ausgelöst wird, wäre es praktisch, wenn genau dieser Datentyp für den ersten Parameter des Delegates verwendet wird. Das ist beim Favoriten-Manager, der im vorherigen Kapitel entwickelt wurde, der Fall: Das Ereignis FavoriteChanged wird ausschließlich vom Favoriten-Manager ausgelöst. Der erste Parameter sollte daher den Datentyp FavoriteManager haben.

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Windows.Forms; 

public class FavoriteManager<TFavorite> : UserControl 
  where TFavorite : Favorite, new() 
{ 
  private ListBox listBox1; 
  List<TFavorite> favs = new List<TFavorite>(); 

  public delegate void FavoriteEventHandler(FavoriteManager<TFavorite> sender, FavoriteEventArgs<TFavorite> e); 
  public event FavoriteEventHandler FavoriteChanged; 

  public FavoriteManager() 
  { 
    InitializeComponent(); 
    listBox1.DisplayMember = "Name"; 
  } 

  public void Add(string name, string url) 
  { 
    TFavorite fav = new TFavorite(); 
    fav.Name = name; 
    fav.URL = new Uri(url); 
    favs.Add(fav); 
    listBox1.DataSource = favs; 
  } 

  public TFavorite[] Favorites 
  { 
    set 
    { 
      favs.Clear(); 
      favs.AddRange(value); 
      listBox1.DataSource = favs; 
    } 

    get { return favs.ToArray(); } 
  } 

  private void InitializeComponent() 
  { 
      this.listBox1 = new System.Windows.Forms.ListBox(); 
      this.SuspendLayout(); 
      // 
      // listBox1 
      // 
      this.listBox1.Dock = System.Windows.Forms.DockStyle.Fill; 
      this.listBox1.FormattingEnabled = true; 
      this.listBox1.Location = new System.Drawing.Point(0, 0); 
      this.listBox1.Name = "listBox"; 
      this.listBox1.Size = new System.Drawing.Size(150, 147); 
      this.listBox1.TabIndex = 0; 
      this.listBox1.SelectedIndexChanged += new System.EventHandler(this.listBox1_SelectedIndexChanged); 
      // 
      // FavoriteManager 
      // 
      this.Controls.Add(this.listBox1); 
      this.Name = "FavoriteManager"; 
      this.ResumeLayout(false); 
  } 

  private void listBox1_SelectedIndexChanged(object sender, EventArgs e) 
  { 
    if (FavoriteChanged != null) 
    { 
      FavoriteChanged(this, new FavoriteEventArgs<TFavorite>((TFavorite)listBox1.SelectedItem)); 
    } 
  } 
} 

Die Ereignisverarbeitung im Favoriten-Manager wurde nun überarbeitet. Während im Delegate FavoriteEventHandler die für die generische Programmierung typischen spitzen Klammern auftauchen, ist FavoriteEventHandler selbst nicht generisch. Das erkennen Sie daran, dass hinter dem Namen FavoriteEventHandler keine Parameter in spitzen Klammern definiert sind. Die spitzen Klammern hinter FavoriteManager und FavoriteEventArgs sind deswegen notwendig, weil diese beiden Klassen generisch sind und Parameter erwarten. Sie werden im Folgenden noch sehen, warum FavoriteEventArgs auch in eine generische Klasse umgewandelt wurde.

Methoden, die an FavoriteChanged gebunden und bei Eintreten des entsprechenden Ereignisses aufgerufen werden, erhalten über den ersten Parameter eine Referenz auf den Favoriten-Manager, der das Ereignis ausgelöst hat. Der Parameter muss dabei nicht mehr von Object gecastet werden, sondern besitzt genau den Datentyp des entsprechenden Favoriten-Managers. Dass die Klasse FavoriteManager ihrerseits generisch ist, ist dabei kein Problem.

Beachten Sie, dass Sie selbst dann, wenn Sie bevorzugt generische Klassen einsetzen, hin und wieder Datentypen casten müssen. So gibt es beispielsweise keine generische Version der Klasse List. Beim Zugriff auf die Eigenschaft SelectedItem erhalten Sie immer ein Objekt vom Typ Object zurück, das Sie gezwungenermaßen durch Casting in den Datentyp umwandeln müssen, den das Objekt hat.

using System; 

public class FavoriteEventArgs<TFavorite> : EventArgs 
  where TFavorite : Favorite 
{ 
  TFavorite fav; 

  public FavoriteEventArgs(TFavorite fav) 
  { 
    this.fav = fav; 
  } 

  public TFavorite RelatedFavorite 
  { 
    get { return fav; } 
  } 
} 

Wie Sie gesehen haben ist die Klasse FavoriteEventArgs nun ebenfalls generisch. Das ist deswegen notwendig, weil der Datentyp für Favoriten nicht mehr zwingend Favorite sein muss. Wenn der Favoriten-Manager mit einem anderen Datentyp als Favorite instantiiert wird, muss folglich auch die Eigenschaft RelatedFavorite in der Klasse FavoriteEventArgs diesen anderen Datentyp haben.


7.5 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Überarbeiten Sie Ihre Lösung zur Aufgabe 4 aus Abschnitt 6.6, „Aufgaben“, indem Sie im Favoriten-Manager die generische Klasse LinkedList verwenden.

    Während Ihnen die Klasse LinkedList ähnlich wie List die Speicherverwaltung abnimmt, ähnelt sie weniger stark einem Array, was Sie zu größeren Änderungen im Code des Favoriten-Managers zwingt. So müssen Sie nicht nur die set- und get-Accessoren der Eigenschaft Favorites überarbeiten. Auch ist es nicht mehr möglich, die Eigenschaft DataSource der Klasse ListBox zu verwenden, da LinkedList nicht das dazu notwendige Interface IList implementiert.