9.10 Kovarianz und Kontravarianz generischer Typen
In Abschnitt 5.1.6 haben wir uns bereits mit der Kovarianz und der Kontravarianz von Delegaten beschäftigt. Kovarianz und Kontravarianz wurden mit .NET 4.0 auch für generische Delegates und Interfaces eingeführt. Allerdings sind nicht alle generischen Delegates und Interfaces von Kovarianz und Kontravarianz betroffen, sondern nur einige wenige »Auserwählte«. Damit wurde es möglich, Code intuitiver zu schreiben.
9.10.1 Kovarianz mit Interfaces
Lassen Sie uns zunächst ansehen, was unter der Kovarianz eines Interfaces zu verstehen ist. Um das zu zeigen, sind die elementaren Klassen unseres Projekts GeometricObjects hervorragend geeignet, die wir auch hier zur Veranschaulichung einsetzen wollen. Stellen Sie sich einfach vor, Sie würden eine Methode DoSomething schreiben, deren Parameter Sie Listen von Circle- oder Rectangle-Objekten übergeben wollen. Eine erste Idee könnte es sein, die Methode wie im folgenden Listing gezeigt zu überladen.
static void DoSomething(IEnumerable<Circle> param)
{
[...]
}
static void DoSomething(IEnumerable<Rectangle> param)
{
[...]
}
Listing 9.19 Methode mit Parameter vom Typ »IEnumerable<T>«
Der Zugriff auf diese Methoden könnte im Hauptprogramm folgendermaßen erfolgen:
static void Main(string[] args) {
List<Circle> objects1 = new List<Circle>();
objects1.Add(new Circle { Radius = 77 });
objects1.Add(new Circle { Radius = 23 });
List<Rectangle> objects2 = new List<Rectangle>();
objects2.Add(new Rectangle { Length = 120, Width = 10 });
objects2.Add(new Rectangle { Length = 80, Width = 20 });
DoSomething(objects1);
DoSomething(objects2);
Console.ReadLine();
}
Listing 9.20 Zugriff auf die Methoden aus Listing 9.19
Welche Operationen sich innerhalb der Methode DoSomething abspielen, ist bei unserer Betrachtung bedeutungslos. Beachten Sie hingegen, dass die Methoden einen Parameter vom Typ der Schnittstelle IEnumerable<T> definieren, der entweder für den Typ Circle oder den Typ Rectangle geprägt ist. Wäre es nicht intuitiver, die Überladung der Methode DoSomething durch eine allgemeingültige Version zu ersetzen, die die gemeinsame Basis GeometricObject angibt? Also ändern wir die Methode wie in Listing 9.21 gezeigt ab.
static void DoSomething(IEnumerable<GeometricObject> param)
{
[...]
}
Listing 9.21 Ersatz der Methoden aus Listing 9.19
Der Code wird in Visual Studio 2010 und 2012 einwandfrei kompiliert und fehlerfrei ausgeführt. Vielleicht steht Ihnen auch noch eine ältere Version von Visual Studio zur Verfügung, beispielsweise Visual Studio 2008. Versuchen Sie, den Code auch hier auszuführen, wird bereits das Kompilieren zu einem Fehler führen. Es funktioniert schlicht und ergreifend nicht.
Betrachten wir noch einmal die beiden Listings 9.19 und 9.21. Dass die Übergabe eines List<Circle>-Objekts an den Parameter der Methode DoSomething, der durch den Typ der Schnittstelle IEnumerable<Circle> beschrieben wird, keine Kopfschmerzen bereiten wird, geht aus der Beschreibung der Generics in diesem Kapitel bereits hervor.
Die Übergabe des List<Circle>-Objekts an den Parameter der Methode DoSomething aus Listing 9.21 entspricht im Grunde genommen der folgenden Zuweisungsoperation:
IEnumerable<GeometricObject> @object = new List<Circle>();
Die Zuweisung erscheint intuitiv, denn sie erinnert uns an die Polymorphie. Dennoch ist sie etwas Besonderes und wurde, wie schon erwähnt, erst mit .NET 4.0 eingeführt. Diese Zuweisungsoperation wird erst durch die kovariante Definition des generischen Typparameters möglich. Kovarianz wird durch die Angabe des Schlüsselwortes out vor dem generischen Typparameter sichergestellt. Nachfolgend sehen Sie die Definition des Interfaces IEnumerable<T>, wie es seit .NET 4.0 definiert ist.
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
Listing 9.22 Die Definition der kovarianten Schnittstelle »IEnumerable<T>«
Kovarianz bedeutet, dass auch ein abgeleiteter Typ anstelle des vom generischen Typparameter definierten verwendet werden kann. Das Schlüsselwort out gibt dabei an, dass der Typparameter nur als Typ einer Rückgabe verwendet werden kann (Ausgabe = Output). Dabei handelt es sich im Fall der IEnumerable<T>-Schnittstelle um die Rückgabe der Methode GetEnumerator.
Sie finden das komplette Beispiel auf der Buch-DVD unter ..\Kapitel 9\KovarianzSample.
Die Klassen Circle, Rectangle und GeometricObject sind in diesem Beispielprogramm auf das Wesentliche reduziert.
9.10.2 Kontravarianz mit Interfaces
Während ein kovarianter Typparameter den Typ der Rückgabe beschreibt, dient ein kontravarianter Typparameter der Typangabe des Übergabearguments an eine Methode. Es ist daher auch naheliegend, dass kontravariante Typparameter durch das Schlüsselwort in ergänzt werden, um zu signalisieren, dass es sich dabei um Eingabetypen handelt. Zu den generischen Schnittstellen mit einem kontravarianten Typparameter gehört auch die Schnittstelle IComparer<T>.
public interface IComparer<in T>
{
int Compare(T x, T y);
}
Listing 9.23 Die Definition der kontravarianten Schnittstelle »IComparer<T>«
Erst die Definition als kontravarianter Typparameter ermöglicht uns eine Vergleichsklasse zu schreiben, deren generischer Typparameter auf GeometricObject festgelegt ist und die dennoch auch von allen Objekten genutzt werden kann, die von GeometricObject abgeleitet sind.
Das folgende Beispielprogramm nutzt das kontravariante Interface IComparer<T>, um eine Liste des Typs List<Circle> zu sortieren.
// Beispiel: ..\Kapitel 9\KontravarianzSample
class Program {
static void Main(string[] args) {
List<Circle> liste = new List<Circle>();
liste.Add(new Circle { Radius = 88 });
liste.Add(new Circle { Radius = 22 });
liste.Add(new Circle { Radius = 42 });
liste.Add(new Circle { Radius = 76 });
liste.Sort(new GeoComparer());
foreach (GeometricObject item in liste)
Console.WriteLine(item.GetArea());
Console.ReadLine();
}
}
class GeoComparer : IComparer<GeometricObject> {
public int Compare(GeometricObject x, GeometricObject y) {
return x.GetArea().CompareTo(y.GetArea());
}
}
Listing 9.24 Beispielprogramm zur Kontravarianz
Es braucht wahrscheinlich kaum noch erwähnt zu werden, dass auch dieses Beispielprogramm in Visual Studio 2008 (oder älter) zu einem Kompilierfehler führt.
9.10.3 Zusammenfassung
Kovarianz wird durch das Schlüsselwort out vor dem generischen Typparameter festgelegt und führt dazu, dass der generische Typparameter nur zur Beschreibung eines Rückgabedatentyps verwendet werden kann. Kontravarianz hingegen definiert einen generischen Typparameter, der nur der Übergabe an eine Methode dient und mit dem Schlüsselwort in verziert wird.
Um den Sachverhalt noch einmal deutlich darzulegen, sollten Sie das folgende Listing der fiktiven Schnittstelle IFactory<T> betrachten.
// CreateInstance verursacht Kompilierfehler
public interface IFactory<in T> {
void DoSomething(T param);
T CreateInstance();
}
Listing 9.25 Fehlerverursachende Schnittstelle mit kontravariantem Typparameter
Der generische Typparameter ist mit in kontravariant definiert. Damit kann die Methode DoSomething korrekt bedient werden, während CreateInstance einen Kompilierfehler verursacht.
Definieren Sie nun einen kovarianten Typparameter (siehe Listing 9.26), wird CreateInstance kein Problem mehr verursachen. Jedoch ist DoSomething nun der Urheber eines Kompilierfehlers, weil die Methode einen kontravarianten Typparameter voraussetzt.
// DoSomething verursacht Kompilierfehler
public interface IFactory<out T> {
void DoSomething(T param);
T CreateInstance();
}
Listing 9.26 Fehlerverursachende Schnittstelle mit kovariantem Typparameter
Damit wird deutlich, dass es keine gleichzeitige Kovarianz und Kontravarianz für einen Parameter gibt. Es ist eine Entscheidung »Entweder-oder«. Es sollte auch klar sein, dass nicht alle Interfaces einen kovarianten oder kontravarianten Typparameter definieren müssen. In der .NET-Klassenbibliothek sind daher aktuell nur IEnumerable<T>, IEnumerator<T>, IQueryable<T> und IGrouping<T,K> kovariant, während zu den Schnittstellen mit kontravarianten generischen Typparametern IComparer<T> und IComparable<T> gerechnet werden.
9.10.4 Generische Delegaten mit varianten Typparametern
In .NET werden mit Action<> und Func<> generische Delegates bereitgestellt, die allgemeinen Anforderungen genügen. Einfach gesagt beschreibt ein Delegat vom Typ Action<> eine void-Methode, also eine Methode ohne Rückgabewert, Func<> hingegen eine Methode mit Rückgabewert.
Mit Action<> können wir Methoden beschreiben, die 1 bis 16 Parameter definieren, mit Func<> Methoden mit 0 bis maximal 16 Parametern. Sehen wir uns exemplarisch die Definition von jeweils einer Version dieser beiden Delegates an.
public delegate TResult Func<in T, out TResult>(T arg)
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2)
Hinsichtlich der beiden Schlüsselwörter in und out gilt dasselbe, was auch schon im Abschnitt zuvor erklärt worden ist: in gibt den Rahmen für die Typen der Eingabeparameter vor, out den Bereich der Typen für den Rückgabewert.
Das folgende Beispielprogramm zeigt den Einsatz dieser beiden Delegates. Das Programm selbst hat keinerlei Logik und soll nur dazu dienen, durch Austausch der Typen der generischen Typparameter die Verhaltensänderung zu untersuchen.
// Beispiel: ..\Kapitel 9\Kovariante_Kontravariante_Delegates
class GeometricObject { }
class Circle : GeometricObject { }
class Rectangle : GeometricObject { }
class Program
{
static void Main(string[] args) {
Func<Circle, Circle> handler1 = DoSomething1;
Action<Rectangle> handler2 = DoSomething2;
}
static Circle DoSomething1(GeometricObject @object) {
return new Circle();
}
static void DoSomething2(GeometricObject @object) { }
}
Listing 9.27 Das Beispielprogramm »Kovariante_Kontravariante_Delegates«
In Abschnitt 5.1.6 habe ich Ihnen bereits die allgemeine Kovarianz und Kontravarianz von Delegaten vorgestellt.
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.