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

149

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

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

Mais changer de stringà intentraînera une erreur de compilation:

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

La raison est expliquée dans MSDN :

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

J'ai recherché et trouvé que certaines questions mentionnaient que la raison était 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?

cuongle
la source
3
voir aussi la réponse d'Eric à ma question similaire: stackoverflow.com/questions/4096299/…
thorn̈
1
duplication possible de cant-convert-value-type-array-to-params-object
nawfal

Réponses:

126

Fondamentalement, la variance s'applique lorsque le CLR peut garantir qu'il n'a pas besoin de modifier la représentation des valeurs. Les références se ressemblent toutes - vous pouvez donc utiliser un IEnumerable<string>comme un IEnumerable<object>sans aucun 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 valeur, cela ne fonctionne pas - pour traiter un IEnumerable<int>comme un 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 l' article de blog d' Eric Lippert sur la représentation et l'identité pour en savoir plus sur ce sujet en général.

EDIT: Après avoir relu le blog d'Eric moi-même, c'est au moins autant une question 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 variables soient de types référence. Pour garantir qu'une conversion de référence de variante préserve toujours l'identité, toutes les conversions impliquant des arguments de type doivent également préserver 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.

Jon Skeet
la source
5
@CuongLe: Eh bien, c'est un détail d'implémentation dans certains sens, mais c'est la raison sous-jacente de la restriction, je crois.
Jon Skeet
2
@ AndréCaron: Le blog d'Eric est important ici - il ne s'agit pas seulement de représentation, mais aussi de préservation de l'identité. Mais la préservation de la représentation signifie que le code généré n'a pas du tout besoin de s'en soucier.
Jon Skeet
1
Précisément, l'identité ne peut pas être préservée car ce intn'est pas un sous-type de object. Le fait qu'un changement de représentation soit nécessaire n'en est qu'une conséquence.
André Caron
3
Comment int n'est-il pas un sous-type d'objet? Int32 hérite de System.ValueType, qui hérite de System.Object.
David Klempfner
1
@DavidKlempfner Je pense que le commentaire de @ AndréCaron est mal formulé. Tout type de valeur tel que Int32a deux formes de représentation, "boxed" et "unboxed". Le compilateur doit insérer du code pour convertir d'une forme à l'autre, même si cela est normalement invisible au niveau du code source. En effet, seule la forme "encadrée" est considérée par le système sous-jacent comme un sous-type de object, mais le compilateur s'occupe automatiquement de cela chaque fois qu'un type valeur est affecté à une interface compatible ou à quelque chose de type object.
Steve le
10

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

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

Vous pouvez considérer le stringscomme 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"

C'est une collection de trois éléments, chacun étant une référence à une chaîne. Vous pouvez convertir ceci 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'objet:

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

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

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 entiers:

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

Cette conversion nécessite plus qu'un casting.

Martin Liversage
la source
2
La boxe 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é, thisfait référence à une structure dont les champs recouvrent ceux de l'objet de tas qui le stocke, plutôt que de faire référence à l'objet qui les contient. Il n'existe aucun moyen propre pour une instance de type valeur encadrée d'obtenir une référence à l'objet de tas englobant.
supercat
7

Je pense que tout commence par la définition du LSP(Principe de substitution de Liskov), 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 valeur, par exemple, intne peuvent pas remplacer objectin C#. Prouver est très simple:

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

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

Tigran
la source
1
Je pense que vous utilisez le bon principe, mais il n'y a aucune preuve à faire: ce intn'est pas un sous-type de objectdonc le principe ne s'applique pas. Votre «preuve» repose sur une représentation intermédiaire Integer, qui est un sous-type de objectet pour laquelle le langage a une conversion implicite ( object obj1=myInt;est en fait étendu à object obj1=new Integer(myInt);).
André Caron
Le langage s'occupe de la conversion correcte entre les types, mais son comportement ne correspond pas à celui que l'on attendrait du sous-type d'objet.
Tigran le
Tout ce que je veux dire, c'est que ce intn'est pas un sous-type de object. De plus, LSP ne s'applique pas car myInt, obj1et se obj2réfère à trois objets différents: un intet deux (cachés) Integers.
André Caron
22
@ André: C # n'est pas Java. Le intmot clé C # est un alias pour les BCL System.Int32, qui est en fait un sous-type de object(un alias de System.Object). En fait, intla classe de base de est System.ValueTypequi est la classe de base System.Object. Essayez d' évaluer l'expression suivante et voir typeof(int).BaseType.BaseType. La raison ReferenceEqualsrenvoie false ici est que le intest encadré dans deux cases séparées, et l'identité de chaque case est différente pour n'importe quelle autre case. Ainsi, deux opérations de boxe donnent toujours deux objets qui ne sont jamais identiques, quelle que soit la valeur encadrée.
Allon Guralnek
@AllonGuralnek: Chaque type de valeur (par exemple System.Int32ou List<String>.Enumerator) représente en fait deux types de choses: un type d'emplacement de stockage et un type d'objet de tas (parfois appelé "type de valeur encadré"). Les emplacements de stockage dont les types dérivent System.ValueTypecontiendront les premiers; les objets de tas dont les types font de même contiendront ce dernier. Dans la plupart des langues, une distribution élargie existe du premier au second, et un rétrécissement du second au premier. Notez que tandis que les types de valeur encadrés ont le même descripteur de type que les emplacements de stockage de type valeur, ...
supercat
3

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

Si vous forcez les types de valeur à être traités comme des types de référence (c.-à-d. Les encadrer, par exemple en y faisant référence via une interface), vous pouvez obtenir la 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 ensemble en mémoire de manière contiguë (directement), alors qu'en tant que tableau de types Reference, seuls la référence (un pointeur) est contiguë en mémoire; les objets pointés sont alloué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 qu'une grande partie du 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 mis en œuvre - souvent simplement par omission d'une vérification de type supplémentaire).

(*) On peut considérer que c'est le même problème ...

Mark Hurd
la source