Covariance et contravariance dans les génériques

Les paramètres de type générique covariant et contravariant fournissent une meilleure flexibilité pour l'assignation et l'utilisation de types génériques. Par exemple, les paramètres de type covariant vous permettent d'effectuer des assignations très similaires au polymorphisme ordinaire. Supposez que vous avez une classe de base et une classe dérivée, Base et Derived. Le polymorphisme vous permet d'assigner une instance de Derived à une variable de type Base. De la même façon, étant donné que le paramètre de type de l'interface IEnumerable<T> est covariant, vous pouvez assigner une instance de IEnumerable<Derived> (IEnumerable(Of Derived) en Visual Basic) à une variable de type IEnumerable<Base>, comme indiqué dans le code suivant.

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;

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 covariance paraît très naturelle car elle est similaire au polymorphisme. 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>.

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

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 :

  • Dans le .NET Framework version 4, 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.

Les sous-sections suivantes décrivent en détail les paramètres de type covariant et contravariant :

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

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

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

  • Définition d'interfaces et de délégués génériques variants

  • Liste des types d'interfaces et des types délégués génériques variants

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

En commençant par .NET Framework 4, plusieurs interfaces génériques ont des paramètres de type covariant ; par exemple : IEnumerable<T>IEnumerator<T>IQueryable<T> et IGrouping<TKey, TElement>. Tous les paramètres de type de ces interfaces sont covariants, les paramètres de type sont utilisés uniquement 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>.

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

Retour au début

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

Depuis le .NET Framework 4, 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.

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

Retour au début

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

Dans le .NET Framework 4, 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.

RemarqueRemarque

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 ce comportement : 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 retourne ; 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, puis la stocke dans la variable f1.

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;

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

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

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

Variance dans les délégués génériques et 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 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>.

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

Retour au début

Définition d'interfaces et de délégués génériques variants

À partir du .NET Framework 4, 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.

RemarqueRemarque

Depuis le .NET Framework version 2.0, le Common Language Runtime prend en charge les annotations de variance sur les paramètres de type générique.Avant le .NET Framework 4, la seule méthode pour définir une classe générique avec ces annotations consiste à utiliser MSIL (Microsoft Intermediate Language), en compilant la classe avec Ilasm.exe (Assembleur MSIL) ou en l'émettant dans un assembly dynamique.

Un paramètre de type covariant est marqué avec le mot clé out (mot clé Out en Visual Basic, + pour l'assembleur MSIL). 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.

RemarqueRemarque

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, - pour l'assembleur MSIL). 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. L'assembleur MSIL n'exécute pas ce type de contrôle, mais une exception TypeLoadException est levée si vous essayez de charger un type qui viole les règles.

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

Retour au début

Liste des types d'interfaces et des types délégués génériques variants

Dans le .NET Framework 4, 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

Action<T> à 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

Func<T, TResult> à 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

Retour au début

Voir aussi

Concepts

Variance dans les délégués (C# et Visual Basic)

Autres ressources

Covariance et contravariance (C# et Visual Basic)