Covariance et contravariance dans les génériques

La covariance et la contravariance sont des termes qui font référence à la possibilité d’utiliser un type plus dérivé (plus spécifique) ou moins dérivé (moins spécifique) que celui spécifié à l’origine. Les paramètres de type générique prennent en charge la covariance et la contravariance afin de fournir une meilleure flexibilité dans l'assignation et l'utilisation des types génériques.

Lorsque vous faites référence à un système de type, la covariance, la contravariance et l'invariance ont les définitions suivantes. Les exemples supposent qu'une classe de base est nommée Base et qu'une classe dérivée est nommée Derived.

  • Covariance

    Vous permet d'utiliser un type plus dérivé que celui spécifié à l'origine.

    Vous pouvez affecter une instance d’une IEnumerable<Derived> à une variable de type IEnumerable<Base>.

  • Contravariance

    Vous permet d'utiliser un type plus générique (moins dérivé) que celui spécifié à l'origine.

    Vous pouvez affecter une instance d’une Action<Base> à une variable de type Action<Derived>.

  • Invariance

    Signifie que vous ne pouvez utiliser que le type spécifié à l’origine. Un paramètre de type générique invariant n’est ni covariant ni contravariant.

    Vous ne pouvez pas attribuer une instance de List<Base> à une variable de type List<Derived> et inversement.

Les paramètres de type covariant vous permettent d'effectuer des assignations très similaires au Polymorphisme ordinaire, comme indiqué dans le code suivant.

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

La classe List<T> implémente l'interface générique IEnumerable<T> , donc List<Derived> (List(Of Derived) en Visual Basic) implémente IEnumerable<Derived>. Le paramètre de type covariant fait le reste.

La contravariance, en revanche, paraît peu intuitive. L'exemple suivant crée un délégué de type Action<Base> (Action(Of Base) en Visual Basic), puis assigne ce délégué à une variable de 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())

Cela peut paraître rétrograde, mais c'est le code de type sécurisé qui est compilé et exécuté. L'expression lambda correspond au délégué auquel elle est assignée, elle définit donc une méthode qui prend un paramètre de type Base et n'a aucune valeur de retour. Le délégué résultant peut être assigné à une variable de type Action<Derived> parce que le paramètre de type T du délégué Action<T> est contravariant. Le code est de type sécurisé parce que T spécifie un type de paramètre. Lorsque le délégué de type Action<Base> est appelé comme s'il était de type Action<Derived>, son argument doit être de type Derived. Cet argument peut toujours être passé sans risque à la méthode sous-jacente, parce que le paramètre de la méthode est de type Base.

En général, un paramètre de type covariant peut être utilisé comme type de retour d'un délégué et les paramètres de type contravariant peuvent être utilisés comme types de paramètres. Pour une interface, les paramètres de type covariant peuvent être utilisés comme types de retour des méthodes de l'interface et les paramètres de type contravariant peuvent être utilisés comme types de paramètres des méthodes de l'interface.

La covariance et la contravariance sont désignées collectivement sous le nom de variation. Un paramètre de type générique qui n'est marqué ni comme étant covariant, ni comme étant contravariant, est appelé indifférent. Récapitulatif des informations relatives à la variance dans le common language runtime :

  • Les paramètres de type variant sont limités aux types d'interfaces génériques et aux types délégués génériques.

  • Un type d'interface générique ou un type délégué générique peut avoir des paramètres de type covariant et contravariant.

  • La variance s'applique uniquement aux types référence ; si vous spécifiez un type valeur pour un paramètre de type variant, ce paramètre de type est indifférent pour le type construit résultant.

  • La variance ne s'applique pas à la combinaison de délégués. Autrement dit, avec deux délégués de types Action<Derived> et Action<Base> (Action(Of Derived) et Action(Of Base) en Visual Basic), il n'est pas possible de combiner le deuxième délégué avec le premier, même si le résultat sera de type sécurisé. La variance permet au deuxième délégué d'être assigné à une variable de type Action<Derived>, mais les délégués peuvent uniquement être combinés si leurs types correspondent exactement.

  • À compter de C# 9, les types de retour covariants sont pris en charge. Une méthode de substitution peut déclarer un type de retour plus dérivé, la méthode qu’il substitue, et une propriété de substitution en lecture seule peut déclarer un type plus dérivé.

Interfaces génériques avec paramètres de type covariant

Plusieurs interfaces génériques ont des paramètres de type covariant. Par exemple : IEnumerable<T>, IEnumerator<T>, IQueryable<T>, and IGrouping<TKey,TElement>. Tous les paramètres de type de ces interfaces sont covariants, les paramètres de type sont donc uniquement utilisés pour les types de retour des membres.

L'exemple suivant illustre les paramètres de type covariant. L'exemple définit deux types : Base a une méthode statique nommée PrintBases qui prend un IEnumerable<Base> (IEnumerable(Of Base) en Visual Basic) et imprime les éléments. Derived hérite de Base. L'exemple crée un List<Derived> vide (List(Of Derived) en Visual Basic) et montre que ce type peut être passé à PrintBases et assigné à une variable de type IEnumerable<Base> sans cast. List<T> implémente IEnumerable<T>, qui a un paramètre de type covariant unique. Le paramètre de type covariant est la raison pour laquelle une instance de IEnumerable<Derived> peut être utilisée au lieu de 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

Interfaces génériques avec des paramètres de type contravariant

Plusieurs interfaces génériques ont des paramètres de type contravariant. Par exemple : IComparer<T>, IComparable<T> et IEqualityComparer<T>. Ces interfaces ont des paramètres de type contravariant uniquement, par conséquent, les paramètres de type sont utilisés uniquement comme types de paramètre dans les membres des interfaces.

L'exemple suivant illustre les paramètres de type contravariant. L'exemple définit une classe abstraite (MustInherit dans Visual Basic) Shape avec une propriété Area . L'exemple définit également une classe ShapeAreaComparer qui implémente IComparer<Shape> (IComparer(Of Shape) dans Visual Basic). L'implémentation de la méthode IComparer<T>.Compare est basée sur la valeur de la propriété Area , de sorte que ShapeAreaComparer peut être utilisé pour trier des objets Shape par zone.

La classe Circle hérite de Shape et remplace Area. L'exemple crée un SortedSet<T> d'objets Circle , à l'aide d'un constructeur qui accepte un IComparer<Circle> (IComparer(Of Circle) dans Visual Basic). Toutefois, au lieu de passer un IComparer<Circle>, l'exemple passe un objet ShapeAreaComparer qui implémente IComparer<Shape>. L'exemple peut passer un comparateur d'un type moins dérivé (Shape) lorsque le code appelle un comparateur d'un type plus dérivé (Circle), parce que le paramètre de type de l'interface générique IComparer<T> est contravariant.

Lorsqu'un nouvel objet Circle est ajouté au SortedSet<Circle>, la méthode IComparer<Shape>.Compare (méthode IComparer(Of Shape).Compare dans Visual Basic) de l'objet ShapeAreaComparer est appelée chaque fois que le nouvel élément est comparé à un élément existant. Le type de paramètre de la méthode (Shape) étant moins dérivé que le type passé (Circle), l'appel garantit la cohérence des types. La contravariance permet à ShapeAreaComparer de trier une collection d'un type unique, ainsi qu'une collection mixte de types, qui dérivent de 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

Délégués génériques avec paramètres de type variant

Les délégués génériques Func, tels que Func<T,TResult>, ont des types de retour covariants et des types de paramètres contravariants. Les délégués génériques Action , tels que Action<T1,T2>, ont des types de paramètres contravariants. Cela signifie que les délégués peuvent être assignés à des variables avec des types de paramètres plus dérivés et (dans le cas des délégués génériques Func ) des types de retour moins dérivés.

Notes

Le dernier paramètre de type générique des délégués génériques Func spécifie le type de la valeur de retour dans la signature du délégué. Il est covariant (mot cléout ), alors que les autres paramètres de type générique sont contravariants (mot cléin ).

Le code suivant illustre cela. La première partie du code définit une classe nommée Base, une classe nommée Derived qui hérite de Base, et une autre classe avec une méthode static (Shared en Visual Basic) nommée MyMethod. La méthode prend une instance de Base et retourne une instance de Derived. (Si l’argument est une instance de Derived, MyMethod le renvoie ; si l’argument est une instance de Base, MyMethod retourne une nouvelle instance de Derived.) Dans Main(), l’exemple crée une instance de Func<Base, Derived> (Func(Of Base, Derived) en Visual Basic) qui représente MyMethod, et la stocke dans la variable 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

La deuxième partie du code indique que le délégué peut être assigné à une variable de type Func<Base, Base> (Func(Of Base, Base) en Visual Basic), car le type de retour est covariant.

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

La troisième partie du code indique que le délégué peut être assigné à une variable de type Func<Derived, Derived> (Func(Of Derived, Derived) en Visual Basic), car le type de paramètre est contravariant.

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

La dernière partie du code indique que le délégué peut être assigné à une variable de type Func<Derived, Base> (Func(Of Derived, Base) en Visual Basic), ce qui combine les effets du type de paramètre contravariant et du type de valeur de retour covariant.

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

Variance dans les délégués non génériques

Dans le code précédent, la signature de MyMethod correspond exactement à la signature du délégué générique construit : Func<Base, Derived> (Func(Of Base, Derived) en Visual Basic). L'exemple montre que ce délégué générique peut être stocké dans des variables ou des paramètres de méthode qui ont des types de paramètres plus dérivés et des types de retour moins dérivés, tant que tous les types délégués sont construits à partir du type délégué générique Func<T,TResult>.

Ceci est un point important. Les effets de la covariance et de la contravariance dans les paramètres de type des délégués génériques sont semblables aux effets de la covariance et de la contravariance dans la liaison de délégués ordinaire (consultez Variance dans les délégués (C#) et Variance dans les délégués (Visual Basic)). Toutefois, la variance dans la liaison de délégués fonctionne avec tous les types délégués, et pas seulement les types délégués génériques qui ont des paramètres de type variant. En outre, la variance dans la liaison de délégués permet de lier une méthode à tout délégué disposant de types de paramètres plus restrictifs et d'un type de retour moins restrictif, alors que l'assignation de délégués génériques fonctionne uniquement si les deux types délégués sont construits à partir de la même définition de type générique.

L'exemple suivant indique les effets combinés de la variance dans la liaison de délégués et de la variance dans les paramètres de type générique. L'exemple définit une hiérarchie de type qui inclut trois types, du moins dérivé (Type1) au plus dérivé (Type3). La variance dans la liaison de délégués ordinaire est utilisée pour lier une méthode avec le type de paramètre Type1 et le type de retour Type3 à un délégué générique avec le type de paramètre Type2 et le type de retour Type2. Le délégué générique résultant est ensuite assigné à une autre variable dont le type délégué générique a un paramètre de type Type3 et le type de retour Type1, à l'aide de la covariance et de la contravariance de paramètres de type générique. La deuxième assignation requiert que le type de variable et le type délégué soient construits à partir de la même définition de type générique, dans ce cas, 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

Définir des interfaces génériques et des délégués de variantes

Visual Basic et Visual C# ont des mots clés qui vous permettent de marquer les paramètres de type générique des interfaces et des délégués comme covariants ou contravariants. En outre, les deux langages prennent en charge les conversions implicites.

Un paramètre de type covariant est marqué avec le mot clé out (mot cléOut en Visual Basic). Vous pouvez utiliser un paramètre de type covariant comme valeur de retour d'une méthode qui appartient à une interface ou comme type de retour d'un délégué. Vous ne pouvez pas utiliser un paramètre de type covariant comme contrainte de type générique pour les méthodes d'interface.

Notes

Si une méthode d'une interface a un paramètre qui est un type délégué générique, un paramètre de type covariant du type d'interface peut être utilisé pour spécifier un paramètre de type contravariant du type délégué.

Un paramètre de type contravariant est marqué avec le mot clé in (mot cléIn en Visual Basic). Vous pouvez utiliser un paramètre de type contravariant comme type d'un paramètre d'une méthode qui appartient à une interface ou comme type d'un paramètre d'un délégué. Vous pouvez utiliser un paramètre de type contravariant comme contrainte de type générique pour une méthode d'interface.

Seuls les types d'interfaces et les types délégués peuvent avoir des paramètres de type variant. Un type d'interface ou un type délégué peut avoir à la fois des paramètres de type covariant et contravariant.

Visual Basic et Visual C# ne vous permettent pas de violer les règles d'utilisation des paramètres de type covariant et contravariant ou d'ajouter des annotations de covariance et de contravariance aux paramètres qui diffèrent des types d'interfaces et des types délégués.

Pour obtenir des informations et un exemple de code, consultez Variance dans les interfaces génériques (C#) et Variance dans les interfaces génériques (Visual Basic).

Liste des types

Les types d'interfaces et les types délégués suivants ont des paramètres de type covariant et/ou contravariant.

Type Paramètres de type covariant Paramètres de type contravariant
Il lance Action<T> sur Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>. Oui
Comparison<T> Oui
Converter<TInput,TOutput> Oui Oui
Func<TResult> Oui
Il lance Func<T,TResult> sur Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>. Oui Oui
IComparable<T> Oui
Predicate<T> Oui
IComparer<T> Oui
IEnumerable<T> Oui
IEnumerator<T> Oui
IEqualityComparer<T> Oui
IGrouping<TKey,TElement> Oui
IOrderedEnumerable<TElement> Oui
IOrderedQueryable<T> Oui
IQueryable<T> Oui

Voir aussi