Programmieren in C#: Einführung
Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.
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.
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.
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.
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.
Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.
Ü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.
Copyright © 2009, 2010 Boris Schäling