9.3 Bedingungen (Constraints) festlegen
9.3.1 Constraints mit der »where«-Klausel
Mit der Definition
public class Stack<T> { [...] }
teilen wir dem Compiler mit, dass der verwaltete Datentyp zur Entwicklungszeit noch unbekannt ist. Der generische Typparameter kann in diesem Fall durch jeden x-beliebigen Datentyp ersetzt werden.
Müssen 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 Listing 9.4 an. Die Klasse Demo<T> enthält die Methode DoSomething, die einen Parameter des Typs T definiert. Was genau die Methode leisten soll, interessiert bei dieser Betrachtung nicht. Jedoch wird innerhalb der Methode auf das an den Parameter übergebene Objekt die Methode Dispose aufgerufen, die aus der Schnittstelle IDisposable stammt.
class Demo<T> {
public void DoSomething(T param) {
[...]
param.Dispose();
[...]
}
}
Listing 9.4 Generische Klasse, die die Methode »IDisposable.Dispose« voraussetzt
Bereits das Kompilieren wird zu einem Fehler führen, da der Typparameter T die Methode Dispose nicht generell beschreibt. Wir haben hier eine Bedingung (Constraint) vorliegen, die vom Typparameter erfüllt werden muss, nämlich die Implementierung der Schnittstelle IDisposable, um die Methode Dispose zu garantieren.
Die Lösung der Problematik ist sehr einfach. Bedingungen an Typparameter werden ähnlich wie in einer SQL-Abfrage mit dem Schlüsselwort where spezifiziert. In unserem fiktiven Szenario müsste demnach die Klasse Demo folgendermaßen implementiert werden:
class Demo<T> where T : IDisposable {
public void DoSomething(T param) {
[...]
param.Dispose();
[...]
}
}
Listing 9.5 Generische Klasse mit einem Constraint
Jetzt ist eine Bedingung festgelegt, die der spätere konkrete Typ erfüllen muss: Er muss die Schnittstelle IDisposable unterstützen.
Mit einem Constraint lassen sich generische Typen einschränken, um damit vorzugeben, wie der generische Typ auszusehen hat, welche Verhaltensweisen erforderlich sind. Diese Typparameter werden auch als gebundene Typparameter bezeichnet (generische Typparameter ohne Constraints heißen entsprechend auch ungebundene Typparameter).
Dabei ist das Festlegen der Constraints äußerst flexibel und gestattet zahlreiche Möglichkeiten. So können Sie – falls erforderlich – auch mehrere Interfaces angeben, die voneinander durch ein Komma getrennt werden:
class Demo<T> where T : IDisposable, ICloneable, IComparable
Jetzt wird vorgeschrieben, dass der generische Typparameter nur durch Typen ersetzt werden kann, die gleichzeitig die drei Schnittstellen IDisposable, ICloneable und IComparable implementieren.
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. Um beispielsweise vorzugeben, dass der generische Typparameter vom Typ GeometricObject (oder davon abgeleitet) sein muss, geben Sie die Klasse hinter where an:
class Demo<T> where T : GeometricObject
Sollten Sie eine Bedingung formulieren, die sowohl eine Klasse als auch eine Schnittstelle vorschreibt, muss die Angabe der Klasse vor der Schnittstelle stehen. Mehrere Klassen anzugeben ist nicht erlaubt.
9.3.2 Typparameter auf Klassen oder Strukturen beschränken
Die Angabe einer Einschränkung ist nicht nur auf konkrete Typen möglich. Sie können auch festlegen, dass der generische Typparameter entweder eine class- oder struct-Definition voraussetzt, z. B.:
class Demo<T> where T : class
{
[...]
}
Listing 9.6 Generischer Typparameter, der auf Referenztypen beschränkt
Mit diesen Constraints lassen sich, allgemein formuliert, Werte- oder Referenztypen vorschreiben. Allerdings müssen Sie dabei berücksichtigen, dass die Bedingung struct nicht erlaubt, dass Nullable-Typen verwendet werden. Mit class ist das andererseits möglich.
Auf Nullable-Typen, die eine weitere Spielart der Generics sind, werden wir in Abschnitt 9.8 noch eingehen.
9.3.3 Mehrere Constraints definieren
Beschreibt eine Klasse mehrere generische Typparameter, lassen sich Bedingungen für jeden einzelnen generischen Typparameter festlegen. Dazu müssen Sie den Constraint für jeden einzelnen Platzhalter mit where einleiten:
public class Demo<T, A> where T : IComparable, ICloneable
where A : IDisposable
{
[...]
}
Listing 9.7 Mehrere generische Typparameter einschränken
9.3.4 Der Konstruktor-Constraint »new()«
Nehmen wir an, Sie möchten in einer generischen Klasse ein Objekt vom Typ des generischen Typparameters erzeugen. Das Problem dabei ist, dass der C#-Compiler nicht weiß, ob die den Typparameter ersetzende Klasse 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<T> where T : new()
{
public T DoSomething() {
return new T();
}
}
Listing 9.8 Generischer Typparameter, der den parameterlosen Konstruktor vorschreibt
Der generische Typparameter kann nunmehr nur durch Objekte konkretisiert werden, die einen öffentlichen, parameterlosen Konstruktor unterstützen. Einen parametrisierten Konstruktor vorzuschreiben ist nicht möglich. Werden mehrere Bedingungen definiert, steht new() grundsätzlich immer am Ende der Aufzählung.
9.3.5 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;
// Problemfall: der Rückgabewert
return null;
}
}
Listing 9.9 Rückgabewert der Methode »Pop« der »Stack<T>«-Klasse
Dieser Ansatz ist richtig, solange der Typparameter durch einen Referenztyp beschrieben wird. Handelt es sich jedoch um einen Wertetyp, wird die Laufzeit in einem Desaster enden, da einem Wertetyp null 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 den Wertetypen zugerechneter Typ ist.
public T Pop() {
pointer--;
if (pointer >= 0)
return elements[pointer];
else {
pointer = 0;
return default(T);
}
}
Listing 9.10 Rückgabewert »default(T)«
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.