Kovarianz und Kontravarianz in Generics

Kovarianz und Kontravarianz sind Begriffe, die auf die Fähigkeit Bezug nehmen, einen stärker abgeleiteten (spezifischeren) oder einen weniger stark abgeleiteten (allgemeineren) Typ zu verwenden als ursprünglich angegeben. Generische Typparameter unterstützen Kovarianz und Kontravarianz und bieten somit mehr Flexibilität beim Zuweisen und Verwenden von generischen Typen.

Wenn Sie auf ein Typsystem verweisen, haben Kovarianz, Kontravarianz und Invarianz die folgenden Definitionen. In den Beispielen wird von der Basisklasse Base und der abgeleiteten Klasse Derivedausgegangen.

  • Covariance

    Ermöglicht die Verwendung eines stärker abgeleiteten Typs als ursprünglich angegeben.

    Sie können eine Instanz von IEnumerable<Derived> einer Variablen des Typs IEnumerable<Base> zuweisen.

  • Contravariance

    Ermöglicht die Verwendung eines generischeren (weniger stark abgeleiteten) Typs als ursprünglich angegeben.

    Sie können eine Instanz von Action<Base> einer Variablen des Typs Action<Derived> zuweisen.

  • Invariance

    Dies bedeutet, Sie können nur den ursprünglich angegebenen Typ verwenden. Ein nicht varianter generischer Typparameter ist weder kovariant noch kontravariant.

    Sie können eine Instanz von List<Base> nicht einer Variablen des Typs List<Derived> zuweisen oder umgekehrt.

Kovariante Typparameter ermöglichen es Ihnen, Zuweisungen vorzunehmen, die normaler Polymorphie stark ähneln, wie im folgenden Code dargestellt.

IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;
Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = 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.

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.

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());
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())

Das erscheint rückständig, ist aber typsicherer Code, der kompiliert und ausgeführt wird. Der Lambdaausdruck 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 invariantbezeichnet. Im Folgenden sehen Sie eine kurze Zusammenfassung der Fakten zur Varianz in der Common Language Runtime:

  • Variante Typparameter sind 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.

  • Ab C# 9 werden kovariante Rückgabetypen unterstützt. Eine überschreibende Methode kann einen stärker abgeleiteten Rückgabetyp deklarieren als die Methode, die überschrieben wird, und eine überschreibende schreibgeschützte Eigenschaft kann einen stärker abgeleiteten Typ deklarieren.

Generische Schnittstellen mit kovarianten Typparametern

Mehrere generische Schnittstellen verfügen über kovariante Typparameter, z. B. IEnumerable<T>, IEnumerator<T>, IQueryable<T> und IGrouping<TKey,TElement>. Alle Typparameter dieser Schnittstellen sind kovariant, sodass sie nur für die Rückgabetypen der Member verwendet werden.

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.

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;
    }
}
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

Generische Schnittstellen mit kontravarianten Typparametern

Mehrere generische Schnittstellen verfügen über kontravariante Typparameter, z. B. IComparer<T>, IComparable<T> und 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 Circle ein neues SortedSet<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 Shapeabgeleitet werden.

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
 */
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

Generische Delegaten mit varianten Typparametern

Die generischen Func-Delegaten (z. B. Func<T,TResult>) verfügen ü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.

Hinweis

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

Dies wird im folgenden Code veranschaulicht. Im ersten Codeabschnitt wird eine Klasse mit dem Namen Base, eine Klasse mit dem Namen Derived , die Baseerbt, und eine weitere Klasse mit einer static -Methode (Shared in Visual Basic) mit dem Namen MyMethoddefiniert. 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 {}
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;
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

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.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());
' Covariant return type.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = 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.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());
' Contravariant parameter type.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = 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.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
' Covariant return type and contravariant parameter type.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())

Varianz bei 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 Varianz in Delegaten (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 Type2zu binden. Der resultierende generische Delegat wird anschließend einer anderen Variablen zugewiesen, deren generischer Delegattyp einen Parameter vom Typ Type3 und den Rückgabetyp Type1hat. 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>.

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());
    }
}
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

Definieren von varianten generischen Schnittstellen und Delegaten

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

Ein kovarianter Typparameter wird mit dem out-Schlüsselwort (Out-Schlüsselwort in Visual Basic) 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.

Hinweis

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) 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.

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

Liste der Typen

Die folgenden Schnittstellen- und Delegattypen verfügen ü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> Ja
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

Siehe auch