159 votes

Pourquoi la covariance et la contravariance ne supportent pas le type de valeur

IEnumerable<T> es covariant 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 a int obtiendra une erreur de compilation :

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

La raison en est expliquée dans MSDN :

La variance 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 que la raison est la boxe entre le type de valeur et le type de référence . Mais je n'ai pas encore bien compris pourquoi la boxe est la raison ?

Quelqu'un pourrait-il expliquer de façon simple et détaillée pourquoi la covariance et la contravariance ne supportent pas le type de valeur et comment boxe affecte cela ?

3 votes

Voir également la réponse d'Eric à ma question similaire : stackoverflow.com/questions/4096299/

1 votes

131voto

Jon Skeet Points 692016

Fondamentalement, la variance s'applique lorsque le CLR peut s'assurer qu'il n'a pas besoin de faire de changement de représentation aux valeurs. Les références se présentent toutes de la même manière - vous pouvez donc utiliser une référence IEnumerable<string> comme un IEnumerable<object> sans aucun changement de représentation ; le code natif lui-même n'a pas besoin de savoir ce que vous faites avec les valeurs, du moment que l'infrastructure a garanti qu'elles seront définitivement valides.

Pour les types de valeur, cela ne fonctionne pas - pour traiter une valeur de type IEnumerable<int> comme un IEnumerable<object> le code qui utilise la séquence doit savoir s'il doit effectuer une conversion de boîte ou non.

Vous devriez lire l'article d'Eric Lippert. article de blog sur la représentation et l'identité pour en savoir plus sur ce sujet en général.

EDIT : Après avoir relu l'article du blog d'Eric, je constate qu'il s'agit au moins autant de identité comme une 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é exigent que tous les arguments de type variable soient des types de référence. Pour s'assurer qu'une conversion de référence variante est toujours préservée par l'identité, toutes les conversions impliquant des arguments de type doivent également être préservées par l'identité. La façon la plus simple de s'assurer que toutes les conversions non triviales sur les arguments de type sont préservatrices d'identité est de les restreindre à des conversions de référence.

0 votes

Merci pour votre réponse, le lien d'Eric Lippert est très utile, je suis toujours un peu confus de votre déclaration. La variance s'applique lorsque le CLR peut garantir qu'il n'a pas besoin d'effectuer de changement de représentation aux valeurs ? c'est le premier critère pour appliquer la variance ?

5 votes

@CuongLe : Eh bien, c'est un détail de mise en œuvre dans certains sens, mais c'est la raison sous-jacente de la restriction, je crois.

0 votes

Je ne pense pas que cela ait quelque chose à voir avec la représentation : int n'est pas un sous-type de object Il n'y a donc pas de conversion covariante à effectuer. En revanche, IEnumerable<Integer> serait un type de retour covariant.

10voto

Martin Liversage Points 43712

Il est peut-être plus facile de comprendre si vous pensez à la représentation sous-jacente (même s'il s'agit en réalité d'un détail de mise en œuvre). Voici une collection de chaînes de caractères :

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

Vous pouvez penser à la strings comme ayant la représentation suivante :

\[0\] : string reference -> "A"
\[1\] : string reference -> "B"
\[2\] : string reference -> "C"

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

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

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

\[0\] : object reference -> "A"
\[1\] : object reference -> "B"
\[2\] : object reference -> "C"

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

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 mettant en boîte les ints :

\[0\] : object reference -> 1
\[1\] : object reference -> 2
\[2\] : object reference -> 3

Cette conversion nécessite plus qu'un moulage.

2 votes

La mise en boîte n'est pas seulement un "détail de mise en œuvre". Les types de valeur encadrés sont stockés de la même manière que les objets de classe et se comportent, pour autant que le monde extérieur puisse le dire, comme des objets de classe. La seule différence est que dans la définition d'un type de valeur encadré, this fait référence à une structure dont les champs recouvrent ceux de l'objet heap qui la stocke, plutôt que de faire référence à l'objet qui les contient. Il n'y a pas de moyen propre pour une instance de type valeur encadrée d'obtenir une référence à l'objet heap qui l'entoure.

8voto

Tigran Points 41381

Je pense que tout commence par la définition de LSP (Principe de Substitution de Liskov), qui climatise :

si q(x) est une propriété prouvable pour les objets x de type T, alors q(y) doit ê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 peut pas se substituer à object en C# . Prouver est très simple :

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

Ce retour false même si nous attribuons le même "référence" à l'objet.

1 votes

Je pense que vous utilisez le bon principe mais il n'y a pas de preuve à faire : int n'est pas un sous-type de object donc le principe ne s'applique pas. Votre "preuve" s'appuie sur une représentation intermédiaire Integer qui est un sous-type de object et pour lesquels le langage dispose d'une conversion implicite ( object obj1=myInt; est actuellement étendu à object obj1=new Integer(myInt) ;).

0 votes

Le langage prend soin d'un casting correct entre les types, mais le comportement des ints ne correspond pas à celui que l'on attendrait du sous-type de l'objet.

0 votes

Ce que je veux dire, c'est que int n'est pas un sous-type de object . De plus, le PSL ne s'applique pas car myInt , obj1 y obj2 font référence à trois objets différents : un int et deux (cachés) Integer s.

3voto

Mark Hurd Points 4746

Cela se résume à un détail de mise en œuvre : 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'est-à-dire en les encadrant, par exemple en y faisant référence via une interface), vous pouvez obtenir une variance.

La manière la plus simple de voir la différence est de considérer simplement une Array Un tableau de types Value est placé en mémoire de manière contiguë (directement), alors qu'un tableau de types Reference ne dispose que de la référence (un pointeur) de manière contiguë en mémoire ; les objets pointés sont alloués séparément.

L'autre problème (connexe)(*) est que (presque) tous les types de référence ont la même représentation à des fins de variance et qu'une grande partie du code n'a pas besoin de connaître la différence entre les types, de sorte que la co et la contre-variance sont possibles (et facilement mises en œuvre - souvent simplement par l'omission d'une vérification de type supplémentaire).

(*) On peut considérer qu'il s'agit de la même question...

-6voto

umlcat Points 2025

Ce n'est peut-être pas la bonne réponse, mais, rappelez-vous que Int32 c'est un type de classe y int es un type primitif . Pour le compilateur, ce sont pas le même type . Dans certains cas, le compilateur permet d'effectuer une conversion cachée ou implicite, qui n'est pas visible pour le programmeur.

Et, il en va de même pour String class type y string primitive type .

Je préfère encore mentionner "référence (s)" comme "objet (s)", et "valeur (s)" comme "type primitif (s)".

Cependant, le compilateur modifie implicitement de nombreux éléments en un format Object Si vous avez besoin d'une version de la base de données, vous pouvez utiliser explicitement la version Objet et Classe des données que vous souhaitez utiliser.

Un U.R.L. avec la spécification ECMA de .Net pour les "types primitifs" :

http://www.jaggersoft.com/csharp_standard/11.1.3.htm Une autre question portant sur un sujet similaire :

Quelle est la différence entre INT, INT16, INT32 et INT64 ?

EDIT : Changement de la majuscule de "Integer" en "Int32".

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X