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 typeIEnumerable<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 typeAction<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 typeList<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>
etAction<Base>
(Action(Of Derived)
etAction(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 typeAction<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 |