Covariantie en contravariantie in generics

Covariantie en contravariantie zijn termen die verwijzen naar de mogelijkheid om een meer afgeleid type (specifieker) of een minder afgeleid type (minder specifiek) te gebruiken dan oorspronkelijk is opgegeven. Algemene typeparameters ondersteunen covariantie en contravariantie om meer flexibiliteit te bieden bij het toewijzen en gebruiken van algemene typen.

Wanneer u verwijst naar een typesysteem, hebben covariantie, contravariantie en invariantie de volgende definities. In de voorbeelden wordt uitgegaan van een basisklasse met de naam BaseDeriveden een afgeleide klasse.

  • Covariance

    Hiermee kunt u een meer afgeleid type gebruiken dan oorspronkelijk is opgegeven.

    U kunt een exemplaar van IEnumerable<Derived> het type toewijzen aan een variabele van het type IEnumerable<Base>.

  • Contravariance

    Hiermee kunt u een meer algemeen (minder afgeleid) type gebruiken dan oorspronkelijk is opgegeven.

    U kunt een exemplaar van Action<Base> het type toewijzen aan een variabele van het type Action<Derived>.

  • Invariance

    Dit betekent dat u alleen het type kunt gebruiken dat oorspronkelijk is opgegeven. Een invariante algemene typeparameter is geen covariant of contravariant.

    U kunt geen exemplaar van List<Base> een variabele van het type List<Derived> of omgekeerd toewijzen.

Met covarianttypeparameters kunt u toewijzingen maken die er ongeveer als gewone Polymorfisme uitzien, zoals wordt weergegeven in de volgende code.

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

De List<T> klasse implementeert de IEnumerable<T> interface, dus List<Derived> (List(Of Derived) in Visual Basic) wordt geïmplementeerd IEnumerable<Derived>. De parameter voor het covarianttype doet de rest.

Contravariantie daarentegen lijkt contraintintief. In het volgende voorbeeld wordt een gemachtigde van het type Action<Base> (Action(Of Base) in Visual Basic) gemaakt en vervolgens toegewezen aan een variabele van het type Action<Derived>.

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

Dit lijkt achterwaarts, maar het is typeveilige code die wordt gecompileerd en uitgevoerd. De lambda-expressie komt overeen met de gemachtigde waaraan deze is toegewezen, dus definieert deze een methode die één parameter van het type gebruikt Base en die geen retourwaarde heeft. De resulterende gemachtigde kan worden toegewezen aan een variabele van het type Action<Derived> omdat de typeparameter T van de Action<T> gemachtigde contravariant is. De code is type-veilig omdat T een parametertype wordt opgegeven. Wanneer de gemachtigde van het type Action<Base> wordt aangeroepen alsof het een gemachtigde van het type Action<Derived>is, moet het argument van het type Derivedzijn. Dit argument kan altijd veilig worden doorgegeven aan de onderliggende methode, omdat de parameter van de methode van het type Baseis.

Over het algemeen kan een covarianttypeparameter worden gebruikt als het retourtype van een gemachtigde en kunnen contravarianttypeparameters worden gebruikt als parametertypen. Voor een interface kunnen covarianttypeparameters worden gebruikt als de retourtypen van de methoden van de interface en kunnen contravarianttypeparameters worden gebruikt als parametertypen van de methoden van de interface.

Covariantie en contravariantie worden gezamenlijk aangeduid als variantie. Een algemene typeparameter die niet is gemarkeerd als covariant of contravariant, wordt invariant genoemd. Een korte samenvatting van feiten over variantie in de algemene taalruntime:

  • Parameters voor varianttypen zijn beperkt tot algemene interface- en algemene gedelegeerdentypen.

  • Een algemene interface of algemeen gemachtigdentype kan zowel covariant- als contravarianttypeparameters hebben.

  • Afwijking is alleen van toepassing op verwijzingstypen; als u een waardetype opgeeft voor een parameter van het varianttype, is die typeparameter invariant voor het resulterende samengestelde type.

  • Variantie is niet van toepassing op combinatie van gemachtigden. Dat wil gezegd, gezien twee gemachtigden van typen Action<Derived> en Action<Base> (Action(Of Derived) en Action(Of Base) in Visual Basic), kunt u de tweede gemachtigde niet combineren met de eerste, hoewel het resultaat veilig zou zijn. Met variantie kan de tweede gemachtigde worden toegewezen aan een variabele van het type Action<Derived>, maar gemachtigden kunnen alleen combineren als hun typen exact overeenkomen.

  • Vanaf C# 9 worden covariant-retourtypen ondersteund. Een onderdrukkingsmethode kan een meer afgeleid retourtype declareren voor de methode die wordt overschreven en een overschrijvende, alleen-lezen eigenschap kan een meer afgeleid type declareren.

Algemene interfaces met parameters voor covarianttypen

Verschillende algemene interfaces hebben covariant-typeparameters, bijvoorbeeld , IEnumerable<T>IEnumerator<T>, IQueryable<T>en IGrouping<TKey,TElement>. Alle typeparameters van deze interfaces zijn covariant, dus de typeparameters worden alleen gebruikt voor de retourtypen van de leden.

In het volgende voorbeeld ziet u parameters voor covarianttypen. In het voorbeeld worden twee typen gedefinieerd: Base heeft een statische methode die een IEnumerable<Base> (IEnumerable(Of Base)in Visual Basic) gebruikt PrintBases en de elementen afdrukt. Derived neemt over van Base. In het voorbeeld wordt een lege List<Derived> (List(Of Derived) in Visual Basic) gemaakt en wordt gedemonstreerd dat dit type kan worden doorgegeven PrintBases aan en toegewezen aan een variabele van het type IEnumerable<Base> zonder cast-conversie. List<T>IEnumerable<T>implementeert, die één parameter voor het covarianttype heeft. De parameter voor het covarianttype is de reden waarom een exemplaar van IEnumerable<Derived> kan worden gebruikt in plaats van IEnumerable<Base>.

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

Algemene interfaces met parameters voor contravarianttypen

Verschillende algemene interfaces hebben parameters voor contravarianttypen; bijvoorbeeld: IComparer<T>, IComparable<T>en IEqualityComparer<T>. Deze interfaces hebben alleen contravariante typeparameters, dus de typeparameters worden alleen gebruikt als parametertypen in de leden van de interfaces.

In het volgende voorbeeld ziet u parameters voor het type contravariant. In het voorbeeld wordt een abstracte klasse (MustInherit in Visual Basic) Shape gedefinieerd met een Area eigenschap. In het voorbeeld wordt ook een ShapeAreaComparer klasse gedefinieerd die (IComparer(Of Shape)in Visual Basic) wordt geïmplementeerd IComparer<Shape> . De implementatie van de IComparer<T>.Compare methode is gebaseerd op de waarde van de Area eigenschap, dus ShapeAreaComparer kan worden gebruikt om objecten te sorteren Shape op gebied.

De Circle klasse neemt Shape over en overschrijft Area. In het voorbeeld wordt een SortedSet<T> object gemaakt met behulp van Circle een constructor die een IComparer<Circle> (IComparer(Of Circle) in Visual Basic) gebruikt. In plaats van een IComparer<Circle>door te geven, geeft het voorbeeld echter een ShapeAreaComparer object door, dat implementeert IComparer<Shape>. In het voorbeeld kan een vergelijking van een minder afgeleid type (Shape) worden doorgegeven wanneer de code een vergelijking van een meer afgeleid type aanroept (Circle), omdat de typeparameter van de IComparer<T> algemene interface contravariant is.

Wanneer een nieuw Circle object wordt toegevoegd aan het SortedSet<Circle>object, wordt de IComparer<Shape>.Compare methode (IComparer(Of Shape).Compare methode in Visual Basic) van het ShapeAreaComparer object telkens aangeroepen wanneer het nieuwe element wordt vergeleken met een bestaand element. Het parametertype van de methode (Shape) is minder afgeleid dan het type dat wordt doorgegeven (Circle), zodat de aanroep veilig is. Met contravariantie ShapeAreaComparer kunt u een verzameling van elk willekeurig type sorteren, evenals een gemengde verzameling typen, die zijn afgeleid van Shape.

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

Algemene gemachtigden met parameters voor varianttypen

De Func algemene gemachtigden, zoals Func<T,TResult>covariant retourtypen en contravariantparametertypen, hebben. De Action algemene gemachtigden, zoals Action<T1,T2>, hebben contravariante parametertypen. Dit betekent dat de gemachtigden kunnen worden toegewezen aan variabelen met meer afgeleide parametertypen en (in het geval van de Func algemene gemachtigden) minder afgeleide retourtypen.

Notitie

De laatste algemene typeparameter van de Func algemene gemachtigden geeft het type retourwaarde op in de handtekening voor gedelegeerden. Het is covariant (out trefwoord), terwijl de andere algemene typeparameters contravariant (in trefwoord) zijn.

De volgende code illustreert dit. Het eerste codefragment definieert een klasse met de naam Base, een klasse die Derived wordt Baseovergenomen en een andere klasse met een static methode (Shared in Visual Basic) met de naam MyMethod. De methode neemt een exemplaar van Base en retourneert een exemplaar van Derived. (Als het argument een instantie is van Derived, MyMethod retourneert het; als het argument een exemplaar is van Base, MyMethod retourneert een nieuw exemplaar van Derived.) In Main()het voorbeeld wordt een instantie gemaakt van Func<Base, Derived> (Func(Of Base, Derived) in Visual Basic) die het vertegenwoordigt MyMethoden opslaat in de variabele f1.

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

Het tweede codefragment laat zien dat de gedelegeerde kan worden toegewezen aan een variabele van het type Func<Base, Base> (Func(Of Base, Base) in Visual Basic), omdat het retourtype covariant is.

// 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())

Het derde codefragment laat zien dat de gemachtigde kan worden toegewezen aan een variabele van het type Func<Derived, Derived> (Func(Of Derived, Derived) in Visual Basic), omdat het parametertype contravariant is.

// 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())

Het laatste stukje code laat zien dat de gemachtigde kan worden toegewezen aan een variabele van het type Func<Derived, Base> (Func(Of Derived, Base) in Visual Basic), waarbij de effecten van het parametertype contravariant en het covariant-retourtype worden gecombineerd.

// 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())

Afwijking in niet-algemene gemachtigden

In de voorgaande code komt de handtekening MyMethod van exact overeen met de handtekening van de samengestelde algemene gemachtigde: Func<Base, Derived> (Func(Of Base, Derived) in Visual Basic). In het voorbeeld ziet u dat deze algemene gemachtigde kan worden opgeslagen in variabelen of methodeparameters met meer afgeleide parametertypen en minder afgeleide retourtypen, zolang alle gedelegeerdentypen zijn samengesteld op basis van het algemene gedelegeerdentype Func<T,TResult>.

Dit is een belangrijk punt. De effecten van covariantie en contravariantie in de typeparameters van algemene gemachtigden zijn vergelijkbaar met de effecten van covariantie en contravariantie in gewone gedelegeerdenbinding (zie Variantie in gemachtigden (C#) en variantie in gedelegeerden (Visual Basic)). Variantie in gedelegeerde binding werkt echter met alle gedelegeerdentypen, niet alleen met algemene gedelegeerdentypen met parameters voor varianttypen. Bovendien maakt variantie in gedelegeerde binding een methode mogelijk om te worden gebonden aan alle gedelegeerden met meer beperkende parametertypen en een minder beperkend retourtype, terwijl de toewijzing van algemene gemachtigden alleen werkt als beide gemachtigdentypen zijn samengesteld uit dezelfde algemene typedefinitie.

In het volgende voorbeeld ziet u de gecombineerde effecten van variantie in gedelegeerde binding en variantie in algemene typeparameters. In het voorbeeld wordt een typehiërarchie gedefinieerd die drie typen bevat, van minst afgeleide (Type1) tot de meest afgeleide (Type3). Variantie in gewone gedelegeerde-binding wordt gebruikt om een methode te binden met een parametertype van Type1 en een retourtype van Type3 een algemene gemachtigde met een parametertype van Type2 en een retourtype van Type2. De resulterende algemene gemachtigde wordt vervolgens toegewezen aan een andere variabele waarvan het algemene gemachtigdetype een parameter van het type Type3 en een retourtype heeft, met behulp van Type1de covariantie en contravariantie van algemene typeparameters. Voor de tweede toewijzing moeten zowel het variabeletype als het gemachtigde type worden samengesteld op basis van dezelfde algemene typedefinitie, in dit geval 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

Algemene variantinterfaces en gemachtigden definiëren

Visual Basic en C# hebben trefwoorden waarmee u de algemene typeparameters van interfaces en gemachtigden kunt markeren als covariant of contravariant.

Een parameter voor het covarianttype wordt gemarkeerd met het out trefwoord (Out trefwoord in Visual Basic). U kunt een covarianttypeparameter gebruiken als de retourwaarde van een methode die deel uitmaakt van een interface of als het retourtype van een gemachtigde. U kunt geen covarianttypeparameter gebruiken als een algemene typebeperking voor interfacemethoden.

Notitie

Als een methode van een interface een parameter heeft die een algemeen gemachtigdentype is, kan een covarianttypeparameter van het interfacetype worden gebruikt om een parameter voor het contravarianttype van het gemachtigdetype op te geven.

Een parameter voor het contravarianttype wordt gemarkeerd met het in trefwoord (In trefwoord in Visual Basic). U kunt een parameter voor een contravarianttype gebruiken als het type parameter van een methode die deel uitmaakt van een interface of als het type parameter van een gemachtigde. U kunt een parameter voor een contravarianttype gebruiken als een algemene typebeperking voor een interfacemethode.

Alleen interfacetypen en gedelegeerdentypen kunnen varianttypeparameters hebben. Een interface- of gemachtigdentype kan zowel covariant- als contravariant-typeparameters hebben.

Visual Basic en C# staan niet toe dat u de regels voor het gebruik van parameters voor covariant- en contravarianttypen schendt of covariantie- en contravariantieaantekeningen toevoegt aan de typeparameters van andere typen dan interfaces en gemachtigden.

Zie Variantie in algemene interfaces (C#) en variantie in algemene interfaces (Visual Basic) voor informatie en voorbeeldcode.

Lijst met typen

De volgende interface- en gemachtigde typen hebben covariant- en/of contravarianttypeparameters.

Type Parameters voor covarianttype Parameters voor contravarianttype
Action<T> tot en met 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> tot en met 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

Zie ook