Por qué la covarianza y la contravarianza no admiten el tipo de valor

149

IEnumerable<T>es covariante pero no admite el tipo de valor, solo el tipo de referencia. El siguiente código simple se compila correctamente:

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

Pero cambiar de stringa intobtendrá un error compilado:

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

La razón se explica en MSDN :

La variación se aplica solo a los tipos de referencia; si especifica un tipo de valor para un parámetro de tipo variante, ese parámetro de tipo es invariable para el tipo construido resultante.

He buscado y encontrado que algunas preguntas mencionan que la razón es el boxeo entre el tipo de valor y el tipo de referencia . Pero todavía no aclara mi mente por qué el boxeo es la razón.

¿Podría alguien dar una explicación simple y detallada de por qué la covarianza y la contravarianza no admiten el tipo de valor y cómo el boxeo afecta esto?

cuongle
fuente
3
vea también la respuesta de Eric a mi pregunta similar: stackoverflow.com/questions/4096299/…
thorn̈
1
posible duplicado de cant-convert-value-type-array-to-params-object
nawfal

Respuestas:

126

Básicamente, la varianza se aplica cuando el CLR puede garantizar que no necesita hacer ningún cambio representativo en los valores. Todas las referencias tienen el mismo aspecto, por lo que puede usar un IEnumerable<string>comoIEnumerable<object> sin ningún cambio en la representación; el código nativo en sí no necesita saber qué está haciendo con los valores, siempre y cuando la infraestructura garantice que definitivamente será válido.

Para los tipos de valor, eso no funciona: tratar un IEnumerable<int>como unIEnumerable<object> , el código que usa la secuencia tendría que saber si realizar una conversión de boxeo o no.

Es posible que desee leer la publicación del blog de Eric Lippert sobre representación e identidad para obtener más información sobre este tema en general.

EDITAR: Habiendo releído la publicación del blog de Eric, al menos se trata tanto de identidad como de representación, aunque los dos están vinculados. En particular:

Esta es la razón por la cual las conversiones covariantes y contravariantes de los tipos de interfaz y delegado requieren que todos los argumentos de tipo variable sean de tipos de referencia. Para garantizar que una conversión de referencia variante siempre preserva la identidad, todas las conversiones que involucran argumentos de tipo también deben preservar la identidad. La forma más fácil de garantizar que todas las conversiones no triviales en los argumentos de tipo conserven la identidad es restringirlas para que sean conversiones de referencia.

Jon Skeet
fuente
55
@CuongLe: Bueno, es un detalle de implementación en algunos sentidos, pero creo que es la razón subyacente de la restricción.
Jon Skeet
2
@ AndréCaron: La publicación del blog de Eric es importante aquí, no es solo la representación, sino también la preservación de la identidad. Pero la preservación de la representación significa que el código generado no necesita preocuparse por esto en absoluto.
Jon Skeet
1
Precisamente, la identidad no se puede preservar porque intno es un subtipo de object. El hecho de que se requiera un cambio de representación es solo una consecuencia de esto.
André Caron
3
¿Cómo es int no un subtipo de objeto? Int32 hereda de System.ValueType, que hereda de System.Object.
David Klempfner el
1
@DavidKlempfner Creo que el comentario de @ AndréCaron está mal redactado. Cualquier tipo de valor como Int32tiene dos formas de representación, "en caja" y "sin caja". El compilador debe insertar código para convertir de un formulario a otro, aunque esto normalmente es invisible a nivel de código fuente. En efecto, el sistema subyacente considera que solo la forma "en caja" es un subtipo de object, pero el compilador se ocupa automáticamente de esto cada vez que se asigna un tipo de valor a una interfaz compatible o algo por el estilo object.
Steve
10

Quizás sea más fácil de entender si piensa en la representación subyacente (aunque esto realmente es un detalle de implementación). Aquí hay una colección de cadenas:

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

Puedes pensar en el strings que tiene la siguiente representación:

[0]: referencia de cadena -> "A"
[1]: referencia de cadena -> "B"
[2]: referencia de cadena -> "C"

Es una colección de tres elementos, cada uno de los cuales es una referencia a una cadena. Puedes lanzar esto a una colección de objetos:

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

Básicamente es la misma representación, excepto que ahora las referencias son referencias de objeto:

[0]: referencia de objeto -> "A"
[1]: referencia de objeto -> "B"
[2]: referencia de objeto -> "C"

La representación es la misma. Las referencias son tratadas de manera diferente; ya no puede acceder a la string.Lengthpropiedad pero aún puede llamar object.GetHashCode(). Compare esto con una colección de entradas:

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

Para convertir esto en un, IEnumerable<object>los datos se deben convertir encuadrando las entradas

[0]: referencia de objeto -> 1
[1]: referencia de objeto -> 2
[2]: referencia de objeto -> 3

Esta conversión requiere más que un yeso.

Martin Liversage
fuente
2
El boxeo no es solo un "detalle de implementación". Los tipos de valores encuadrados se almacenan de la misma manera que los objetos de clase, y se comportan, por lo que el mundo exterior puede decir, como los objetos de clase. La única diferencia es que, dentro de la definición de un tipo de valor en caja, se thisrefiere a una estructura cuyos campos se superponen a los del objeto de almacenamiento dinámico que lo almacena, en lugar de referirse al objeto que los contiene. No hay una forma limpia para que una instancia de tipo de valor en caja obtenga una referencia al objeto de montón adjunto.
supercat
7

Creo que todo comienza desde la definición del LSP(Principio de sustitución de Liskov), que climas:

si q (x) es una propiedad demostrable sobre los objetos x del tipo T, entonces q (y) debería ser verdadera para los objetos y del tipo S donde S es un subtipo de T.

Pero los tipos de valor, por ejemplo int, no pueden sustituir a objectin C#. Probar es muy simple:

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

Esto vuelve falseincluso si asignamos la misma "referencia" al objeto.

Tigran
fuente
1
Creo que está utilizando el principio correcto, pero no hay pruebas que hacer: intno es un subtipo de, objectpor lo que el principio no se aplica. Su "prueba" se basa en una representación intermedia Integer, que es un subtipo de objecty para el cual el lenguaje tiene una conversión implícita ( object obj1=myInt;actualmente se expande a object obj1=new Integer(myInt);).
André Caron
El lenguaje se encarga de la conversión correcta entre los tipos, pero su comportamiento no corresponde al que esperaríamos del subtipo de objeto.
Tigran
Todo mi punto es precisamente que intno es un subtipo de object. Además, LSP no se aplica porque myInt, obj1y se obj2refiere a tres objetos diferentes: unoint y dos (ocultos) Integers.
André Caron
22
@ André: C # no es Java. La intpalabra clave de C # es un alias para los BCL System.Int32, que de hecho es un subtipo de object(un alias de System.Object). De hecho, intla clase base es System.ValueTypequién es la clase base System.Object. Trate de evaluar la siguiente expresión y mira typeof(int).BaseType.BaseType. La razón por la que ReferenceEqualsdevuelve falso aquí es que intestá encuadrado en dos cuadros separados, y la identidad de cada cuadro es diferente para cualquier otro cuadro. Por lo tanto, dos operaciones de boxeo siempre producen dos objetos que nunca son idénticos, independientemente del valor encuadrado.
Allon Guralnek
@AllonGuralnek: cada tipo de valor (p. Ej. System.Int32O List<String>.Enumerator) en realidad representa dos tipos de cosas: un tipo de ubicación de almacenamiento y un tipo de objeto de almacenamiento dinámico (a veces denominado "tipo de valor en caja"). Ubicaciones de almacenamiento cuyos tipos se derivan deSystem.ValueType contendrán las primeras; los objetos del montón cuyos tipos hacen lo mismo contendrán el último. En la mayoría de los idiomas, existe un reparto cada vez más amplio del primero, y un reparto estrecho del último al primero. Tenga en cuenta que, si bien los tipos de valores en caja tienen el mismo descriptor de tipo que las ubicaciones de almacenamiento de tipo de valor, ...
supercat
3

Se reduce a un detalle de implementación: los tipos de valor se implementan de manera diferente a los tipos de referencia.

Si fuerza que los tipos de valor se traten como tipos de referencia (es decir, encuadrarlos, por ejemplo, al referirse a ellos a través de una interfaz), puede obtener la variación.

La forma más fácil de ver la diferencia es simplemente considerar Array: una matriz de tipos de Valor se unen en memoria contigua (directamente), mientras que como una matriz de Tipos de referencia solo tiene la referencia (un puntero) contiguamente en la memoria; los objetos que se apuntan se asignan por separado.

El otro problema (relacionado) (*) es que (casi) todos los tipos de Referencia tienen la misma representación para propósitos de varianza y mucho código no necesita conocer la diferencia entre los tipos, por lo que es posible la covarianza y la contravarianza (y fácilmente implementado, a menudo simplemente por omisión de verificación de tipo adicional).

(*) Puede verse que es el mismo problema ...

Mark Hurd
fuente