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 string
a int
obtendrá 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?
fuente
Respuestas:
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:
fuente
int
no es un subtipo deobject
. El hecho de que se requiera un cambio de representación es solo una consecuencia de esto.Int32
tiene 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 deobject
, 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 estiloobject
.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:
Puedes pensar en el
strings
que tiene la siguiente representación: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:
Básicamente es la misma representación, excepto que ahora las referencias son referencias de objeto:
La representación es la misma. Las referencias son tratadas de manera diferente; ya no puede acceder a la
string.Length
propiedad pero aún puede llamarobject.GetHashCode()
. Compare esto con una colección de entradas:Para convertir esto en un,
IEnumerable<object>
los datos se deben convertir encuadrando las entradasEsta conversión requiere más que un yeso.
fuente
this
refiere 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.Creo que todo comienza desde la definición del
LSP
(Principio de sustitución de Liskov), que climas:Pero los tipos de valor, por ejemplo
int
, no pueden sustituir aobject
inC#
. Probar es muy simple:Esto vuelve
false
incluso si asignamos la misma "referencia" al objeto.fuente
int
no es un subtipo de,object
por lo que el principio no se aplica. Su "prueba" se basa en una representación intermediaInteger
, que es un subtipo deobject
y para el cual el lenguaje tiene una conversión implícita (object obj1=myInt;
actualmente se expande aobject obj1=new Integer(myInt)
;).int
no es un subtipo deobject
. Además, LSP no se aplica porquemyInt
,obj1
y seobj2
refiere a tres objetos diferentes: unoint
y dos (ocultos)Integer
s.int
palabra clave de C # es un alias para los BCLSystem.Int32
, que de hecho es un subtipo deobject
(un alias deSystem.Object
). De hecho,int
la clase base esSystem.ValueType
quién es la clase baseSystem.Object
. Trate de evaluar la siguiente expresión y miratypeof(int).BaseType.BaseType
. La razón por la queReferenceEquals
devuelve falso aquí es queint
está 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.System.Int32
OList<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, ...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 ...
fuente