web-dev-qa-db-fra.com

Pourquoi la covariance et la contravariance ne prennent pas en charge le type de valeur

IEnumerable<T> est co-variant mais il ne prend pas en charge le type de valeur, seulement le type de référence. Le code simple ci-dessous est compilé avec succès:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

Mais passer de string à int obtiendra une erreur compilée:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

La raison est expliquée dans MSDN :

L'écart ne s'applique qu'aux types de référence; si vous spécifiez un type de valeur pour un paramètre de type variant, ce paramètre de type est invariant pour le type construit résultant.

J'ai cherché et trouvé que certaines questions mentionnaient la raison de la boxe entre le type de valeur et le type de référence . Mais cela ne m'éclaircit pas encore beaucoup pourquoi la boxe est la raison?

Quelqu'un pourrait-il donner une explication simple et détaillée pourquoi la covariance et la contravariance ne prennent pas en charge le type de valeur et comment la boxe affecte cela?

142
cuongle

Fondamentalement, la variance s'applique lorsque le CLR peut garantir qu'il n'a pas besoin d'apporter de changement de représentation aux valeurs. Les références se ressemblent toutes - vous pouvez donc utiliser un IEnumerable<string> en tant que IEnumerable<object> sans changement de représentation; le code natif lui-même n'a pas du tout besoin de savoir ce que vous faites avec les valeurs, tant que l'infrastructure a garanti qu'elle sera définitivement valide.

Pour les types de valeur, cela ne fonctionne pas - pour traiter un IEnumerable<int> en tant que IEnumerable<object>, le code utilisant la séquence devrait savoir s'il faut effectuer une conversion de boxe ou non.

Vous voudrez peut-être lire le - billet de blog sur la représentation et l'identité d'Eric Lippert pour en savoir plus sur ce sujet en général.

EDIT: Après avoir relu moi-même le billet de blog d'Eric, il s'agit au moins autant d'identité que de représentation, bien que les deux soient liés. En particulier:

C'est pourquoi les conversions covariantes et contravariantes des types d'interface et de délégué nécessitent que tous les arguments de type variant soient de type référence. Pour garantir qu'une conversion de référence de variante conserve toujours l'identité, toutes les conversions impliquant des arguments de type doivent également être préservant l'identité. Le moyen le plus simple de garantir que toutes les conversions non triviales sur les arguments de type préservent l'identité est de les limiter à des conversions de référence.

120
Jon Skeet

Il est peut-être plus facile à comprendre si vous pensez à la représentation sous-jacente (même si c'est vraiment un détail d'implémentation). Voici une collection de chaînes:

IEnumerable<string> strings = new[] { "A", "B", "C" };

Vous pouvez considérer le strings comme ayant la représentation suivante:

 [0]: référence de chaîne -> "A" 
 [1]: référence de chaîne -> "B" 
 [2]: référence de chaîne -> "C" 

Il s'agit d'une collection de trois éléments, chacun étant une référence à une chaîne. Vous pouvez convertir cela en une collection d'objets:

IEnumerable<object> objects = (IEnumerable<object>) strings;

Fondamentalement, c'est la même représentation sauf que maintenant les références sont des références d'objet:

 [0]: référence d'objet -> "A" 
 [1]: référence d'objet -> "B" 
 [2]: référence d'objet -> "C" 

La représentation est la même. Les références sont juste traitées différemment; vous ne pouvez plus accéder à la propriété string.Length mais vous pouvez toujours appeler object.GetHashCode(). Comparez cela à une collection d'ts:

IEnumerable<int> ints = new[] { 1, 2, 3 };
 [0]: int = 1 
 [1]: int = 2 
 [2]: int = 3 

Pour convertir cela en un IEnumerable<object>, Les données doivent être converties en encadrant les entrées:

 [0]: référence d'objet -> 1 
 [1]: référence d'objet -> 2 
 [2]: référence d'objet -> 3 

Cette conversion nécessite plus qu'un cast.

9
Martin Liversage

Je pense que tout part de la définition de LSP (Liskov Substitution Principle), qui clime:

si q(x) est une propriété prouvable sur les objets x de type T alors q(y) devrait être vrai pour les objets y de type S où S est un sous-type de T.

Mais les types de valeurs, par exemple int ne peuvent pas remplacer object dans C#. Prouver est très simple:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

Cela renvoie false même si nous attribuons la même "référence" à l'objet.

7
Tigran

Cela se résume à un détail d'implémentation: les types de valeur sont implémentés différemment des types de référence.

Si vous forcez les types de valeur à être traités comme des types de référence (c.-à-d. Que vous les encadrez, par exemple en vous y référant via une interface), vous pouvez obtenir une variance.

La façon la plus simple de voir la différence est simplement de considérer un Array: un tableau de types Value est mis en mémoire de manière contiguë (directement), alors qu'un tableau de types Reference n'a que la référence (un pointeur) contiguë dans Mémoire; les objets pointés sont attribués séparément.

L'autre problème (lié) (*) est que (presque) tous les types de référence ont la même représentation à des fins de variance et beaucoup de code n'a pas besoin de connaître la différence entre les types, donc la co-et la contre-variance sont possibles (et facilement implémenté - souvent juste en omettant une vérification de type supplémentaire).

(*) Il peut s'agir du même problème ...

3
Mark Hurd