1. Introduction▲
Une des nouveautés de .NET 4.0 est l'introduction de la covariance et contravariance.
Sous ces termes un peu ésotériques se cachent des concepts assez simples en réalité. Je vais donc tenter de démystifier tout ça avec cet article et des exemples simples.
Accrochez-vous, c'est parti !
2. Covariance▲
La covariance, qu'est-ce ?
Une définition générale pourrait être : conversion du plus large au plus fin. (Traduction littérale de l'article anglais de Wikipédia sur la variance : (http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science))
Nous voilà bien avancés ! Si on tente de traduire cela en langage de développeur : conversion du plus dérivé vers le moins dérivé. Mais encore. Un exemple sera plus clair.
2-A. Exemple de covariance▲
Ce code :
IEnumerable<
object
>
test =
new
List<
string
>(
);
Ne compilera pas sous .NET 3.5 et donnera ce message d'erreur : « Cannotimplicitlyconvert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.IEnumerable<object>'. An explicit conversion exists (are youmissing a cast?) »
Pourtant List<T> implémente bien IEnumerable<T>, et string est bien dérivé d'object. Mais comme IEnumerable<T> n'est pas covariant, un IEnumerable <string> ne peut être considéré comme un IEnumerable<object> ! (cf. définition : du plus dérivé vers le moins dérivé).
Par contre, à partir de 4.0, pas de problème car IEnumerable<T> est covariant : le paramètre de sortie de type T peut être considéré comme un type U si T dérive de U.
Tentons une définition de la covariance : compatibilité d'assignation, pour une interface ou un délégué ayant un ou plusieurs paramètres génériques de sortie(voir plus bas) de type T1,T2... avec le même interface ou délégué ayant les paramètres génériques de sortie U1,U2... si T1,T2... dérivent de U1,U2...
Toutes les interfaces ne sont pas covariantes en .NET 4.0. Voici la liste de la liste de celles qui le sont :
- IEnumerable<out T>(T est covariant) ;
- IEnumerator<out T>(T est covariant) ;
- IQueryable<out T>(T est covariant) ;
- IGrouping<out TKey, out TElement>(TKeyetTElementsontcovariants) ;
- tous les délégués Func<in T1... , out TResult> (TResult est covariant).
Comme on le voit, la covariance ne concerne pas que les interfaces. Les délégués peuvent aussi être covariants :
public
Func<
string
>
stringfunc;
Func<
object
>
objectfunc =
stringfunc;
Comme vu dans cet exemple, je peux utiliser une fonction qui renvoie T comme une fonction qui renvoie U si T est dérivé de U.
2-B. Création d'interfaces et de délégués covariants▲
Pour rendre une interface ou un délégué covariant, il faut ajouter le mot-clé out (http://msdn.microsoft.com/en-us/library/dd469487.aspx) au type paramètre concerné. Comme le mot-clé l'indique, celui-ci ne peut alors se trouver uniquement en paramètre de sortie.
2-B-1. Exemple d'interface covariant▲
public
interface
IFruitTree<
out
T>
{
T GetFruit
(
);
}
Des classes implémentant cette interface :
public
class
Tree {
}
public
class
Fruit {
}
public
class
Orange :
Fruit {
}
public
class
OrangeTree :
Tree,
IFruitTree<
Orange>
{
public
Orange GetFruit
(
)
{
return
new
Orange
(
);
}
}
Et cette fonction :
public
void
CatchFruit
(
IFruitTree<
Fruit>
fruittree)
{
///
}
IFruitTree<out T> étant covariant sur T, nous pouvons passer, au lieu d'un objet implémentant IFruitTree<Fruit>, un objet implémentant un type plus dérivé, comme IFruitTree<Orange> :
var
o =
new
OrangeTree
(
);
CatchFruit
(
o);
Comme on le voit, cela simplifie les choses et permet une plus grande réutilisation des composants.
2-B-2. Exemple de délégué covariant▲
public
delegate
T CovariantDelegate<
out
T>(
);
void
ShowCovariance
(
)
{
var
stringdelegate =
new
CovariantDelegate<
string
>(
ReturnString);
CovariantDelegate<
object
>
objectdelegate =
stringdelegate;
object
result =
objectdelegate.
Invoke
(
);
}
public
string
ReturnString
(
)
{
return
"Covariance"
;
}
Comme déjà dit, un paramètre en out ne peut être qu'un paramètre de sortie. Par exemple :
public
interface
IsInhabited<
out
T>
{
T DoSomething
(
T param);
}
Donnera une erreur de compilation :
Invalid variance: The type parameter 'T' must be contravariantly valid on ' Co_Contravariant.IsInhabited <T>. DoSomething ( T)'. 'T' is covariant.
Pourquoi uniquement en paramètre de sortie ? Pour tenter d'expliquer simplement, si T est uniquement en sortie, cela ne pose pas de problème, car il n'y a pas de problème d'assigner à un type T (object, par exemple), le résultat d'une méthode qui renvoie un type U plus dérivé (string, par exemple).
Par contre, si on acceptait <T> en entrée, on pourrait dès lors se retrouver dans un cas comme celui-ci :
public
Func<
Exception,
int
>
OKFunc;
public
Func<
object
,
int
>
ProblemsAhead;
void
Main
(
)
{
OKFunc =
CountStackTrace;
ProblemsAhead =
OKFunc;
...
var
something =
ProblemsAhead
(
new
object
(
));
}
public
int
CountStackTrace
(
Exception e)
{
return
e.
StackTrace.
Count
(
);
}
3. Contravariance▲
Le pendant de la covariance est la contravariance. Pour reprendre le même type de définition : conversion du plus fin au plus large. Traduction : du moins dérivé au plus dérivé.
Ça a l'air moins naturel à première vue, mais c'est tout à fait logique comme on va le constater :
3-A. Exemple de contravariance▲
Imaginons que nous ayons ceci :
public
class
Animal {}
public
class
Dog :
Animal {}
Ainsi qu'un comparateur entre Animal qui implémente IEqualityComparer<T> (qui est contravariant) :
public
class
CompareAnimals :
IEqualityComparer<
Animal>
{
public
bool
Equals
(
Animal x,
Animal y)
{
.
}
public
int
GetHashCode
(
Animal obj)
{
.
}
}
Si nous avons une liste de Dog, ceci est valide :
var
dogs =
new
List<
Dog>(
);
var
distinct =
dogs.
Distinct
(
new
CompareAnimals
(
));
Alors que la signature de Distinct dans ce cas est : Distinct<Dog>(IEqualityComparer<Dog> Comparer).
Nous passons donc un type moins dérivé (IEqualityComparer<Animal>) à la place d'un plus dérivé (IEqualityComparer<Dog>). Ça peut paraitre paradoxal mais revenons à une définition plus affinée : conversion des paramètres d'entrées du moins dérivé vers le plus dérivé.
En effet : la covariance est uniquement sur les paramètres de sortie et la contravariance est uniquement sur les paramètres d'entrées. Distinct va appeler la méthode Equal(Animal x, Animal y) en lui passant deux instances de Dog alors qu'elle attend deux instances d'Animal. Mais, vu que Dog dérive d'Animal, ça ne pose pas de problème ! Ce qui semble contre nature est en fait tout à fait logique.
Tentons maintenant une définition de la contravariance : compatibilité d'assignation, pour un interface ou un délégué ayant un ou plusieurs paramètres génériques d'entréede type T1,T2... avec le même interface ou délégué ayant les paramètres génériques d'entrée U1,U2... si U1,U2... dérivent de T1,T2...
Voilà la liste des interfaces contravariants de .NET 4.0 :
- IComparer<in T>(T est contravariant) ;
- IEqualityComparer<in T>(T est contravariant) ;
- IComparable<in T>(T est contravariant) ;
- tous les délégués Action<in T>, Action<in T1...>(T, T1... sont contravariants) ;
- tous les délégués Func<in T1..., outTResult>(T, T1... sont contravariants).
Comme pour la covariance, les délégués peuvent aussi être contravariants :
public
Action<
object
>
OKAction;
void
ShowCovariance
(
)
{
OKAction =
new
Action<
object
>((
o) =>
o.
ToString
(
));
DoSomethingWithAction
(
OKAction);
}
static
void
DoSomethingWithAction
(
Action<
string
>
action)
{
action
(
"Contravariance"
);
}
3-B. Création d'interfaces et de délégués contravariants▲
Pour créer une interface ou un délégué contravariant, il faut utiliser le mot-clé in (http://msdn.microsoft.com/en-us/library/dd469484.aspx). Comme le nom l'indique aussi, les paramètres en in ne peuvent être que des paramètres d'entrées.
3-B-1. Exemple d'interface contravariant▲
public
interface
ICompareWeigth<
in
T>
where
T:
Animal
{
int
Weight {
get
;
set
;
}
bool
SameWeight
(
T animal);
}
public
class
CompareAnimalWeight :
ICompareWeigth<
Animal>
{
public
int
CompareWeight (
Animal animal1,
Animal animal2)
{
// do something;
}
}
Si on a cette fonction :
public
int
CompareDogWeight
(
ICompareWeigth<
Dog>
comparison,
Dog dog1,
Dog dog2)
{
return
comparison.
CompareWeight
(
dog1,
dog2);
}
Ceci est valide :
CompareDogWeight
(
new
CompareAnimalWeight
(
),
new
Dog
(
),
new
Dog
(
));
3-B-2. Exemple de délégué contravariant▲
public
delegate
void
ContravariantDelegate<
in
T>(
T param);
public
void
DoThing
(
object
o){}
public
void
TestContravariance
(
ContravariantDelegate<
string
>
del)
{
del.
Invoke
(
"Contravariance"
);
}
void
ShowContravariance
(
)
{
ContravariantDelegate<
object
>
OKDelegate =
DoThing;
TestContravariance
(
OKDelegate);
}
4. Remarques▲
- La covariance et contravariance ne concernent que les paramètres de type générique des délégués (depuis .NET 2.0) et des interfaces ainsi que les types tableaux ([]).
Notons, que, pour les types tableaux, c'est à déconseiller car on peut alors arriver à ce genre de choses :
string
[]
stringarray =
new
[]{
"Quelle est la réponse ?"
};
object
[]
objectarray =
stringarray;
objectarray[
0
]
=
42
;
Qui compilera bien, mais il y aura une ArrayTypeMismatchException à l'exécution.
- La variance dans les génériques n'est supportée que par les types référence, et pas les types valeurs :
IEnumerable<
int
>
intlist =
new
List<
int
>(
);
IEnumerable<
object
>
objectlist =
intlist;
// Ne compile pas
- Le support de la variance n'est implémenté qu'à partir de la version 5 de Silverlight.
- Le support de la variance n'est pas implémenté sous Windows Phone 7.
- Il n'est pas possible d'avoir un paramètre à la fois covariant et contravariant. Par contre, il est possible d'avoir un (ou plusieurs) paramètre(s) covariant(s), et un (ou plusieurs) contravariant(s). Dans le framework, les classes Func<in T... ,out TResult> le sont. Un exemple :
Func<
object
,
string
>
func =
(
o) =>
o.
ToString
(
);
Func<
string
,
object
>
CoContra =
func;
5. Conclusion▲
Voilà, j'espère avoir démystifié ces deux noms barbares.
Le fait de rendre ses interfaces ou délégués covariants et/ou contravariants permet de les réutiliser plus facilement, je suis donc du même avis que Jeffrey Richter, qui recommande donc d'utiliser in et out quand c'est possible afin de permettre une réutilisation dans plus de scénarios.
6. Remerciements▲
Je tiens à remercier Philippe Vialatte et toute l'équipe de Développez pour leur aide durant la rédaction de cet article.
Un grand merci à Claude Leloup et Maxime Gault pour la correction orthographique.