c# .net c#-4.0 covariance contravariance

c# - Por qué la covarianza y la contravarianza no son compatibles con el tipo de valor



.net c#-4.0 (4)

Básicamente, la variación se aplica cuando el CLR puede garantizar que no es necesario realizar ningún cambio de representación en los valores. Todas las referencias tienen el mismo aspecto, por lo que puede usar una IEnumerable<string> IEnumerable<object> sin ningún cambio en la representación; el código nativo en sí no necesita saber lo que está haciendo con los valores en absoluto, siempre y cuando la infraestructura haya garantizado que definitivamente será válido.

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

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

EDITAR: Al haber 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 que 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 de variante siempre preserve la identidad, todas las conversiones que involucren argumentos de tipo también deben preservar la identidad. La manera más fácil de garantizar que todas las conversiones no triviales en los argumentos de tipo protejan la identidad es restringirlas para que sean conversiones de referencia.

IEnumerable<T> es IEnumerable<T> pero no admite el tipo de valor, solo el tipo de referencia. El código simple a continuación se compila con éxito:

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;

El motivo se explica en MSDN :

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

He buscado y he encontrado que algunas preguntas mencionaron que la razón es el boxeo entre el tipo de valor y el tipo de referencia . Pero todavía no aclaro mucho 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 contravariancia no son compatibles con el tipo de valor y cómo el boxeo afecta esto?


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

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

Pero los tipos de valor, por ejemplo, int no pueden ser sustituto del object en C# . Probar es muy simple:

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

Esto devuelve false incluso si asignamos la misma "referencia" al objeto.


Quizás sea más fácil de entender si piensas 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 que las strings tienen la siguiente representación:

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

Es una colección de tres elementos, cada uno es una referencia a una cadena. Puede convertir esto en 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 objetos:

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

La representación es la misma. Las referencias solo se tratan de manera diferente; ya no puede acceder a la propiedad string.Length , pero aún puede llamar a object.GetHashCode() . Compare esto con una colección de ints:

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

[0] : int = 1 [1] : int = 2 [2] : int = 3

Para convertir esto a un IEnumerable<object> los datos deben convertirse al IEnumerable<object> :

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

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


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

Si fuerza a los tipos de valores a tratarse como tipos de referencia (es decir, boxéelos, p. Ej., Haciendo referencia a ellos a través de una interfaz), puede obtener la varianza.

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

La otra cuestión (relacionada) (*) es que (casi) todos los tipos de Referencia tienen la misma representación para fines de varianza y que gran parte del código no necesita conocer la diferencia entre los tipos, por lo que es posible la contra-varianza (y implementado - a menudo solo por omisión de verificación de tipo adicional).

(*) Se puede ver que es el mismo problema ...