Kovarianz und Kontravarianz in Generika

Kovariante und kontravariante generische Typparameter bieten mehr Flexibilität beim Zuweisen und Verwenden von generischen Typen. Kovariante Typparameter ermöglichen es Ihnen z. B., Zuweisungen vorzunehmen, die normaler Polymorphie sehr ähneln. Angenommen, Sie haben eine Basisklasse und eine abgeleitete Klasse namens Base und Derived. Mithilfe der Polymorphie können Sie eine Instanz von Derived einer Variable des Base-Typs zuzuweisen. Da der Typparameter der IEnumerable<T>-Schnittstelle kovariant ist, können Sie gleichermaßen wie im folgenden Code dargestellt eine Instanz von IEnumerable<Derived> (IEnumerable(Of Derived) in Visual Basic) einer Variable des IEnumerable<Base>-Typs zuweisen.

Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = d
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;

Die List<T>-Klasse implementiert die generische IEnumerable<T>-Schnittstelle. List<Derived> (List(Of Derived) in Visual Basic) implementiert daher IEnumerable<Derived>. Der kovariante Typparameter ist für den Rest zuständig.

Kovarianz scheint sehr natürlich, da sie wie Polymorphie erscheint. Kontravarianz erscheint dagegen kontraintuitiv. Im folgenden Beispiel wird ein Delegat des Action<Base>-Typs (Action(Of Base) in Visual Basic) erstellt und dann einer Variable des Action<Derived>-Typs zugewiesen.

Dim b As Action(Of Base) = Sub(target As Base) 
                               Console.WriteLine(target.GetType().Name)
                           End Sub
Dim d As Action(Of Derived) = b
d(New Derived())
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

Dies erscheint rückläufig, ist aber typsicherer Code, der kompiliert und ausgeführt wird. Der Lambda-Ausdruck entspricht dem ihm zugewiesenen Delegaten und definiert daher eine Methode, die einen Parameter des Base-Typs akzeptiert und keinen Wert zurückgibt. Der resultierende Delegat kann einer Variable des Action<Derived>-Typs zugewiesen werden, da der Typparameter T des Action<T>-Delegaten kontravariant ist. Der Code ist typsicher, da T einen Parametertyp angibt. Wenn der Delegat des Action<Base>-Typs wie ein Delegat des Action<Derived>-Typs aufgerufen wird, muss sein Argument vom Derived-Typ sein. Dieses Argument kann immer sicher an die zugrunde liegende Methode übergeben werden, da der Parameter der Methode vom Base-Typ ist.

Im Allgemeinen können kovariante Typparameter als Rückgabetyp eines Delegaten und kontravariante Typparameter als Parametertypen verwendet werden. Für eine Schnittstelle können kovariante Typparameter als Rückgabetypen der Methoden der Schnittstelle verwendet werden und kontravariante Typparameter als Parametertypen der Methoden der Schnittstelle.

Kovarianz und Kontravarianz werden zusammen als Varianz bezeichnet. Ein generischer Typparameter, der nicht als kovariant oder kontravariant markiert ist, wird als invariant bezeichnet. Im Folgenden sehen Sie eine kurze Zusammenfassung der Fakten zur Varianz in der Common Language Runtime:

  • In .NET Framework, Version 4sind variante Typparameter auf generische Schnittstellen und generische Delegattypen beschränkt.

  • Eine generische Schnittstelle oder ein generischer Delegattyp kann sowohl kovariante, als auch kontravariante Typparameter haben.

  • Varianz gilt nur für Verweistypen. Wenn Sie für einen varianten Typparameter einen Werttyp angeben, ist dieser Typparameter für den resultierenden konstruierten Typ invariant.

  • Varianz gilt nicht für eine Delegatkombination. Bei zwei Delegaten vom Typ Action<Derived> und Action<Base> (Action(Of Derived) und Action(Of Base) in Visual Basic) können Sie folglich den zweiten Delegaten nicht mit dem ersten kombinieren, obwohl das Ergebnis typsicher wäre. Bei Varianz kann der zweite Delegat einer Variable des Action<Derived>-Typs zugewiesen werden, Delegaten können aber nur kombiniert werden, wenn ihre Typen genau überstimmen.

In den folgenden Unterabschnitten werden Kovariante und kontravariante Typparameter ausführlich beschrieben:

  • Generische Schnittstellen mit kovarianten Typparametern

  • Generische Schnittstellen mit kontravarianten generischen Typparametern

  • Generische Delegaten mit varianten Typparametern

  • Definieren von varianten generischen Schnittstellen und Delegaten

  • Liste der varianten generischen Schnittstellen und Delegattypen

Generische Schnittstellen mit kovarianten Typparametern

Ab .NET Framework 4 verfügen mehrere generische Schnittstellen über kovariante Typparameter. Beispiele: IEnumerable<T>, IEnumerator<T>, IQueryable<T> und IGrouping<TKey, TElement>. Alle Typparameter dieser Schnittstellen sind kovariant. Die Typparameter werden daher nur für die Rückgabetypen der Member verwendet. 

Im folgenden Beispiel werden kovariante Typparameter veranschaulicht. Im Beispiel werden zwei Typen definiert: Base verfügt über eine statische Methode mit dem Namen PrintBases, die einen IEnumerable<Base> (IEnumerable(Of Base) in Visual Basic) erwartet und die Elemente ausgibt. Derived erbt von Base. Im Beispiel wird ein leerer List<Derived> (List(Of Derived) in Visual Basic) erstellt, um zu veranschaulichen, dass dieser Typ an PrintBases übergeben und ohne Umwandlung einer Variablen vom Typ IEnumerable<Base> zugewiesen werden kann. List<T> implementiert IEnumerable<T>, die über einen einzelnen kovarianten Typparameter verfügt. Aufgrund des kovarianten Typparameters kann eine Instanz von IEnumerable<Derived> anstelle von IEnumerable<Base> verwendet werden.

Imports System.Collections.Generic

Class Base
    Public Shared Sub PrintBases(ByVal bases As IEnumerable(Of Base))
        For Each b As Base In bases
            Console.WriteLine(b)
        Next
    End Sub    
End Class

Class Derived
    Inherits Base

    Shared Sub Main()
        Dim dlist As New List(Of Derived)()

        Derived.PrintBases(dlist)
        Dim bIEnum As IEnumerable(Of Base) = dlist
    End Sub
End Class
using System;
using System.Collections.Generic;

class Base
{
    public static void PrintBases(IEnumerable<Base> bases)
    {
        foreach(Base b in bases)
        {
            Console.WriteLine(b);
        }
    }
}

class Derived : Base
{
    public static void Main()
    {
        List<Derived> dlist = new List<Derived>();

        Derived.PrintBases(dlist);
        IEnumerable<Base> bIEnum = dlist;
    }
}

Zurück nach oben

Generische Schnittstellen mit kontravarianten generischen Typparametern

Beginnend mit .NET Framework 4 verfügen mehrere generische Schnittstellen über kontravariante Typparameter, z. B. IComparer<T>, IComparable<T>, and IEqualityComparer<T>. Diese Schnittstellen verfügen nur über kontravariante Typparameter. Die Typparameter werden daher nur in den Membern der Schnittstellen als Parametertypen verwendet.

Im folgenden Beispiel werden kontravariante Typparameter veranschaulicht. Im Beispiel wird eine abstrakte (MustInherit in Visual Basic) Shape-Klasse mit einer Area-Eigenschaft definiert. Außerdem wird eine ShapeAreaComparer-Klasse definiert, die IComparer<Shape> (IComparer(Of Shape) in Visual Basic) implementiert. Die Implementierung der IComparer<T>.Compare-Methode basiert auf dem Wert der Area-Eigenschaft, sodass ShapeAreaComparer zum Sortieren von Shape-Objekten nach Bereich verwendet werden kann.

Die Circle-Klasse erbt von Shape und überschreibt Area. Im Beispiel wird ein SortedSet<T> von Circle-Objekten erstellt, wobei ein Konstruktor verwendet wird, der IComparer<Circle> (IComparer(Of Circle) in Visual Basic) akzeptiert. Anstelle von IComparer<Circle> wird jedoch ein ShapeAreaComparer-Objekt übergeben, das IComparer<Shape> implementiert. Im Beispiel kann ein Vergleich eines weniger stark abgeleiteten Typs (Shape) übergeben werden, wenn der Code einen Vergleich eines stärker abgeleiteten Typs (Circle) verlangt, da der Typparameter der generischen IComparer<T>-Schnittstelle kontravariant ist.

Wenn SortedSet<Circle> ein neues Circle-Objekt hinzugefügt wird, wird die IComparer<Shape>.Compare-Methode (IComparer(Of Shape).Compare-Methode in Visual Basic) des ShapeAreaComparer-Objekts immer dann aufgerufen, wenn das neue Element mit einem vorhandenen Element verglichen wird. Der Parametertyp der Methode (Shape) ist weniger stark abgeleitet als der Typ, der übergeben wird (Circle). Deshalb ist der Aufruf typsicher. Kontravarianz ermöglicht es ShapeAreaComparer, eine Auflistung eines einzelnen Typs sowie eine Auflistung von gemischten Typen zu sortieren, die von Shape abgeleitet werden.

Imports System.Collections.Generic

MustInherit Class Shape
    Public MustOverride ReadOnly Property Area As Double
End Class

Class Circle
    Inherits Shape

    Private r As Double 
    Public Sub New(ByVal radius As Double)
        r = radius
    End Sub 
    Public ReadOnly Property Radius As Double
        Get
            Return r
        End Get
    End Property
    Public Overrides ReadOnly Property Area As Double
        Get
            Return Math.Pi * r * r
        End Get
    End Property
End Class

Class ShapeAreaComparer
    Implements System.Collections.Generic.IComparer(Of Shape)

    Private Function AreaComparer(ByVal a As Shape, ByVal b As Shape) As Integer _
            Implements System.Collections.Generic.IComparer(Of Shape).Compare
        If a Is Nothing Then Return If(b Is Nothing, 0, -1)
        Return If(b Is Nothing, 1, a.Area.CompareTo(b.Area))
    End Function
End Class

Class Program
    Shared Sub Main()
        ' You can pass ShapeAreaComparer, which implements IComparer(Of Shape),
        ' even though the constructor for SortedSet(Of Circle) expects 
        ' IComparer(Of Circle), because type parameter T of IComparer(Of T)
        ' is contravariant.
        Dim circlesByArea As New SortedSet(Of Circle)(New ShapeAreaComparer()) _
            From { New Circle(7.2), New Circle(100), Nothing, New Circle(.01) }

        For Each c As Circle In circlesByArea
            Console.WriteLine(If(c Is Nothing, "Nothing", "Circle with area " & c.Area))
        Next
    End Sub
End Class

' This code example produces the following output:
'
'Nothing
'Circle with area 0.000314159265358979
'Circle with area 162.860163162095
'Circle with area 31415.9265358979
using System;
using System.Collections.Generic;

abstract class Shape
{
    public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
    private double r;
    public Circle(double radius) { r = radius; }
    public double Radius { get { return r; }}
    public override double Area { get { return Math.PI * r * r; }}
}

class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
    int IComparer<Shape>.Compare(Shape a, Shape b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.Area.CompareTo(b.Area);
    }
}

class Program
{
    static void Main()
    {
        // You can pass ShapeAreaComparer, which implements IComparer<Shape>,
        // even though the constructor for SortedSet<Circle> expects 
        // IComparer<Circle>, because type parameter T of IComparer<T> is
        // contravariant.
        SortedSet<Circle> circlesByArea = 
            new SortedSet<Circle>(new ShapeAreaComparer()) 
                { new Circle(7.2), new Circle(100), null, new Circle(.01) };

        foreach (Circle c in circlesByArea)
        {
            Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
        }
    }
}

/* This code example produces the following output:

null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
 */

Zurück nach oben

Generische Delegaten mit varianten Typparametern

In .NET Framework 4 verfügen die generischen Func-Delegaten (z. B. Func<T, TResult>) über kovariante Rückgabetypen und über kontravariante Parametertypen. Die generischen Action-Delegaten, z. B. Action<T1, T2>, verfügen über kontravariante Parametertypen. Das bedeutet, dass die Delegaten Variablen mit weiter abgeleiteten Parametertypen und (im Fall von generischen Func-Delegaten) weniger abgeleiteten Rückgabetypen zugewiesen werden können.

HinweisHinweis

Der letzte generische Typparameter der generischen Func-Delegaten gibt den Typ des Rückgabewerts in der Signatur des Delegaten an.Er ist kovariant (Schlüsselwort out), wohingegen die anderen generischen Typparameter kontravariant sind (Schlüsselwort in).

Dies wird im folgenden Code veranschaulicht. Im ersten Codeabschnitt wird eine Klasse mit dem Namen Base, eine Klasse mit dem Namen Derived, die Base erbt, und eine weitere Klasse mit einer static-Methode (Shared in Visual Basic) mit dem Namen MyMethod definiert. Die Methode akzeptiert eine Instanz von Base und gibt eine Instanz von Derived zurück. (Wenn das Argument eine Instanz von Derived ist, gibt MyMethod diese zurück. Wenn das Argument eine Instanz von Base ist, gibt MyMethod eine neue Instanz von Derived zurück.) Im Beispiel wird in Main() eine Instanz von Func<Base, Derived> (Func(Of Base, Derived) in Visual Basic) erstellt, die MyMethod darstellt und in der Variablen f1 gespeichert wird.

Public Class Base 
End Class
Public Class Derived
    Inherits Base
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal b As Base) As Derived 
        Return If(TypeOf b Is Derived, b, New Derived())
    End Function

    Shared Sub Main() 
        Dim f1 As Func(Of Base, Derived) = AddressOf MyMethod
public class Base {}
public class Derived : Base {}

public class Program
{
    public static Derived MyMethod(Base b)
    {
        return b as Derived ?? new Derived();
    }

    static void Main() 
    {
        Func<Base, Derived> f1 = MyMethod;

Der zweite Codeabschnitt zeigt, dass der Delegat einer Variablen vom Typ Func<Base, Base> (Func(Of Base, Base) in Visual Basic) zugewiesen werden kann, da der Rückgabetyp kovariant ist.

' Covariant return type.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = f2(New Base())
// Covariant return type.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());

Der dritte Codeabschnitt zeigt, dass der Delegat einer Variablen vom Typ Func<Derived, Derived> (Func(Of Derived, Derived) in Visual Basic) zugewiesen werden kann, da der Parametertyp kontravariant ist.

' Contravariant parameter type.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = f3(New Derived())
// Contravariant parameter type.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());

Der letzte Codeabschnitt zeigt, dass der Delegat einer Variablen vom Typ Func<Derived, Base> (Func(Of Derived, Base) in Visual Basic) zugewiesen werden kann, wobei die Auswirkungen des kontravarianten Parametertyps und des kovarianten Rückgabewerttyps kombiniert werden.

' Covariant return type and contravariant parameter type.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())
// Covariant return type and contravariant parameter type.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());

Varianz in generischen und nicht generischen Delegaten

Im obigen Code passt die Signatur von MyMethod exakt zur Signatur des erstellten generischen Delegaten Func<Base, Derived> (Func(Of Base, Derived) in Visual Basic). Das Beispiel zeigt, dass dieser generische Delegat in Variablen oder Methodenparametern mit weiter abgeleiteten Parametertypen und weniger abgeleiteten Rückgabetypen gespeichert werden kann, solange alle Delegattypen aus dem generischen Delegattyp Func<T, TResult> erstellt werden.

Dies ist ein wichtiger Punkt. Kovarianz und Kontravarianz haben in den Typparametern generischer Delegaten ähnliche Auswirkungen wie bei der normalen Delegatbindung (siehe Varianz in Delegaten (C# und Visual Basic)). Die Varianz bei der Delegatbindung funktioniert jedoch bei allen Delegattypen und nicht nur bei generischen Delegattypen, die über variante Typparameter verfügen. Darüber hinaus kann durch die Varianz bei der Delegatbindung eine Methode an einen beliebigen Delegaten gebunden werden, der restriktivere Parametertypen und einen weniger restriktiven Rückgabetyp verwendet, wohingegen die Zuweisung generischer Delegaten nur funktioniert, wenn beide Delegattypen aus der gleichen generischen Typdefinition erstellt wurden.

Im folgenden Beispiel werden die kombinierten Effekte von Varianz in der Delegatbindung und Varianz bei generischen Typparametern veranschaulicht. Im Beispiel wird eine Typhierarchie definiert, die drei Typen beinhaltet, vom am wenigsten abgeleiteten Typ (Type1) bis zum am weitesten abgeleiteten Typ (Type3). Bei der normalen Delegatbindung wird die Varianz verwendet, um eine Methode mit dem Parametertyp Type1 und dem Rückgabetyp Type3 an einen generischen Delegaten mit dem Parametertyp Type2 und dem Rückgabetyp Type2 zu binden. Der resultierende generische Delegat wird anschließend einer anderen Variablen zugewiesen, deren generischer Delegattyp einen Parameter vom Typ Type3 und den Rückgabetyp Type1 hat. Hierbei werden Kovarianz und Kontravarianz generischer Typparameter verwendet. Bei der zweiten Zuweisung müssen sowohl der Variablentyp als auch der Delegattyp mit der gleichen generischen Typdefinition erstellt werden, in diesem Fall Func<T, TResult>.

Public Class Type1 
End Class
Public Class Type2
    Inherits Type1
End Class
Public Class Type3
    Inherits Type2
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal t As Type1) As Type3
        Return If(TypeOf t Is Type3, t, New Type3())
    End Function

    Shared Sub Main() 
        Dim f1 As Func(Of Type2, Type2) = AddressOf MyMethod

        ' Covariant return type and contravariant parameter type.
        Dim f2 As Func(Of Type3, Type1) = f1
        Dim t1 As Type1 = f2(New Type3())
    End Sub
End Class
using System;

public class Type1 {}
public class Type2 : Type1 {}
public class Type3 : Type2 {}

public class Program
{
    public static Type3 MyMethod(Type1 t)
    {
        return t as Type3 ?? new Type3();
    }

    static void Main() 
    {
        Func<Type2, Type2> f1 = MyMethod;

        // Covariant return type and contravariant parameter type.
        Func<Type3, Type1> f2 = f1;
        Type1 t1 = f2(new Type3());
    }
}

Zurück nach oben

Definieren von varianten generischen Schnittstellen und Delegaten

Beginnend mit .NET Framework 4 verfügen Visual Basic und C# über Schlüsselwörter, mit denen die generischen Typparameter von Schnittstellen und Delegaten als kovariant oder kontravariant gekennzeichnet werden können.

HinweisHinweis

Beginnen mit .NET Framework, Version 2.0, unterstützt die Common Language Runtime Varianzkennzeichnungen für generische Typparameter.Vor .NET Framework 4 war Microsoft Intermediate Language (MSIL) die einzige Möglichkeit, eine generische Klasse zu definieren, die über diese Kennzeichnungen verfügt, entweder durch das Kompilieren der Klasse mit Ilasm.exe (MSIL-Assembler) oder durch das Ausgeben in einer dynamischen Assembly.

Ein kovarianter Typparameter wird mit dem out-Schlüsselwort (Out-Schlüsselwort in Visual Basic, + für den MSIL-Assembler) gekennzeichnet. Sie können einen kovarianten Typparameter als Rückgabewert einer Methode verwenden, die zu einer Schnittstelle gehört, oder als Rückgabetyp eines Delegaten. Sie können einen kovarianten Typparameter nicht als generische Typeinschränkung für Schnittstellenmethoden verwenden.

HinweisHinweis

Wenn eine Methode einer Schnittstelle über einen Parameter verfügt, der ein generischer Delegattyp ist, kann ein kovarianter Typparameter des Schnittstellentyps verwendet werden, um einen kontravarianten Typparameter des Delegattyps anzugeben.

Ein kontravarianter Typparameter wird mit dem in-Schlüsselwort (In-Schlüsselwort in Visual Basic, - für den MSIL-Assembler) gekennzeichnet. Sie können einen kontravarianten Typparameter als Typ eines Parameters einer Methode verwenden, die zu einer Schnittstelle gehört, oder als Typ eines Parameters eines Delegaten. Sie können einen kontravarianten Typparameter als generische Typeinschränkung für Schnittstellenmethoden verwenden.

Nur Schnittstellentypen und Delegattypen können über variante Typparameter verfügen. Eine Schnittstelle oder ein Delegattyp kann sowohl kovariante, als auch kontravariante Typparameter haben.

In Visual Basic und C# müssen die Regeln zum Verwenden von kovarianten und kontravarianten Typparametern eingehalten werden, und es ist nicht möglich, Kovarianz- und Kontravarianzkennzeichnungen zu den Typparametern anderer Typen als Schnittstellen- und Delegattypen hinzuzufügen. Der MSIL-Assembler führt keine solchen Überprüfungen aus, es wird aber eine TypeLoadException ausgelöst, wenn Sie versuchen, einen Typ zu laden, der gegen die Regeln verstößt.

Weitere Informationen und einen Beispielcode finden Sie unter Varianz in generischen Schnittstellen (C# und Visual Basic).

Zurück nach oben

Liste der varianten generischen Schnittstellen und Delegattypen

In .NET Framework 4 verfügen die folgenden Schnittstellen- und Delegattypen über kovariante und/oder kontravariante Typparameter. 

Typ

Kovariante Typparameter

Kontravariante Typparameter

Action<T> bis Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>

Ja

Comparison<T>

Ja

Converter<TInput, TOutput>

Ja

Ja

Func<TResult>

Ja

Func<T, TResult> bis Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult>

Ja

Ja

IComparable<T>

Ja

Predicate<T>

Yes

IComparer<T>

Ja

IEnumerable<T>

Ja

IEnumerator<T>

Ja

IEqualityComparer<T>

Ja

IGrouping<TKey, TElement>

Ja

IOrderedEnumerable<TElement>

Ja

IOrderedQueryable<T>

Ja

IQueryable<T>

Ja

Zurück nach oben

Siehe auch

Konzepte

Varianz in Delegaten (C# und Visual Basic)

Weitere Ressourcen

Kovarianz und Kontravarianz (C# und Visual Basic)