2010-02-05 20 views
16

Genérico Varianza en C# 4.0 ha sido implementado de tal manera que es posible escribir lo siguiente sin una excepción (que es lo que sucedería en C 3.0 #):Genérico Varianza en C# 4.0

List<int> intList = new List<int>(); 
List<object> objectList = intList; 

[no funcional Ejemplo: Véase la respuesta de Jon Skeet]

poco asistí a una conferencia en la que Jon Skeet dio una excelente visión general de la varianza genérica, pero no estoy seguro de que estoy recibiendo por completo - entiendo el significado del in y out palabras clave cuando se trata de contra y co-varianza, pero tengo curiosidad por lo que sucede detrás de escena.

¿Qué ve el CLR cuando se ejecuta este código? ¿Está convirtiendo implícitamente el List<int> en List<object> o simplemente está integrado en que ahora podemos convertir tipos derivados a tipos principales?

Fuera de interés, ¿por qué no se introdujo esto en versiones anteriores y cuál es el beneficio principal, es decir, el uso en el mundo real?

Más información sobre este post de Varianza genérico (pero la pregunta es extremadamente anticuado, en busca de bienes, hasta a la fecha de la información)

Respuesta

20

No, su ejemplo no funcionaría por tres razones:

  • Las clases (como List<T>) son invariables; solo delegados e interfaces son la variante
  • Para que la varianza funcione, la interfaz solo debe usar el parámetro tipo en una dirección (en contravarianza, fuera de covarianza)
  • Los tipos de valor no son compatibles como argumentos de tipo para varianza - así que no hay converstion de IEnumerable<int> a IEnumerable<object> por ejemplo

(El código de falla al compilar en C# 3.0 y 4.0 - no hay excepción.)

así que este haría trabajo:

IEnumerable<string> strings = new List<string>(); 
IEnumerable<object> objects = strings; 

El CLR solo utiliza la referencia, sin cambios: no se crean nuevos objetos. Entonces, si llama al objects.GetType(), seguirá recibiendo List<string>.

Creo que no se presentó antes porque los diseñadores de idiomas todavía tenían que resolver los detalles de cómo exponerlo: ha estado en el CLR desde v2.

Los beneficios son los mismos que en otras ocasiones en las que desea poder utilizar un tipo como otro. Para usar el mismo ejemplo que utilicé el último sábado, si tienes algo que implemente IComparer<Shape> para comparar formas por área, es una locura que no puedas usar eso para ordenar un List<Circle> - si puede comparar dos formas, ciertamente puede comparar dos círculos A partir de C# 4, habría una conversión contravariante de IComparer<Shape> a IComparer<Circle> para que pudiera llamar al circles.Sort(areaComparer).

+0

Ah, señaló. Tendré que descargar y echar un vistazo a los ejemplos del último sábado y jugar con él. El concepto en sí mismo tiene sentido, seguro, solo estoy tratando de entender la ideología del uso del concepto en situaciones del mundo real. Muchas gracias por la respuesta. –

+0

@Daniel: No hay problema - disculpe que claramente no lo expliqué lo suficientemente bien el sábado :) (Había mucho que cubrir, sin duda ...) –

+0

Oh, no es para nada Jon - todo se movía un poco rápido y no he estado expuesto, bueno, a ninguna de las nuevas características de C# 4 - Estaba garabateando notas como un loco. Parece que tendré que pedir la segunda edición de C# en profundidad :) –

8

Como curiosidad, ¿por qué no fue este introducido en las versiones anteriores

Las primeras versiones (1.x) de .NET no tenía genéricos en absoluto, por lo que la varianza genérica estaba lejos apagado.

Se debe tener en cuenta que en todas las versiones de .NET, hay una covarianza de matriz. Por desgracia, es insegura covarianza:

Apple[] apples = new [] { apple1, apple2 }; 
Fruit[] fruit = apples; 
fruit[1] = new Orange(); // Oh snap! Runtime exception! Can't store an orange in an array of apples! 

La co- y contra-varianza en C# 4 es seguro, y evita este problema.

¿cuál es el principal beneficio - es decir, real uso mundial?

Muchas veces en código, que está llamando una API espera un tipo amplificada de base (por ejemplo IEnumerable<Base>), pero todo lo que tienes es un tipo de derivado amplificada (por ejemplo IEnumerable<Derived>).

En C# 2 y C# 3, necesitaría convertir manualmente a IEnumerable<Base>, aunque debería "simplemente funcionar". Co y contra-varianza lo hace "solo funciona".

p.s. Totalmente apesta que la respuesta de Skeet es comer todos mis puntos de rep. ¡Maldito seas, Skeet! :-) Parece que es answered this before, sin embargo.

+1

Para que quede claro: Co y contravariancia * siempre * se ha soportado en la CLI (donde "siempre" significa "al menos desde v2"). Simplemente no estaba * expuesto * en C# hasta C# 4.0. Pero, p. Eiffel.NET siempre lo ha soportado, aunque las bibliotecas AFAIK tampoco se han anotado correctamente. (No sé por qué, en realidad. No hubiera sido * demasiado * difícil crear una herramienta de reescritura de IL que simplemente tome una lista de interfaces co y contravariantes y arroje los bits correctos en los metadatos, incluso si pudiera en el pasado no expresé esto en C#, en el cual está escrito el BCL) –

14

Algunas consideraciones adicionales.

Lo que hace el CLR, consulte cuando este código se ejecuta

Como Jon y otros han señalado correctamente, no estamos haciendo variación en las clases, sólo las interfaces y delegados. Entonces en tu ejemplo, el CLR no ve nada; ese código no compila. Si lo fuerza a compilar insertando suficientes conversiones, se bloquea en el tiempo de ejecución con una excepción de lanzamiento incorrecto.

Ahora, todavía es una pregunta razonable preguntar cómo funciona la variación detrás de las escenas cuando funciona. La respuesta es: la razón por la que estamos restringiendo esto a los argumentos de tipo de referencia que parametrizan los tipos de interfaz y delegado es para que no ocurra nada entre bastidores. Cuando usted dice

object x = "hello"; 

lo que sucede detrás de las escenas es la referencia a la cadena se ha quedado atascado en la variable del tipo de objeto sin modificaciones. Los bits que constituyen una referencia a una cadena son bits legales para hacer referencia a un objeto, por lo que aquí no es necesario que suceda. El CLR simplemente deja de pensar en esos bits como refiriéndose a una cadena y comienza a pensar que se refieren a un objeto.

Cuando dicen:

IEnumerator<string> e1 = whatever; 
IEnumerator<object> e2 = e1; 

misma cosa. No pasa nada. Los bits que hacen una referencia a un enumerador de cadena son los mismos que los que hacen una referencia a un enumerador de objetos.Hay un poco más de magia que entra en juego cuando se hace un molde, por ejemplo:

Ahora el CLR debe generar un cheque que E1 realidad no aplicar esa interfaz, y que el registro tiene que ser inteligente sobre el reconocimiento de la varianza.

Pero la razón por la que podemos lograr que las interfaces variantes sean simplemente conversiones no operativas es porque compatibilidad de asignación regular es así. ¿Para qué vas a usar e2?

object z = e2.Current; 

Devuelve los bits que son una referencia a una cadena. Ya hemos establecido que esos son compatibles con el objeto sin cambios.

¿Por qué no se introdujo anteriormente? Teníamos otras funciones que hacer y un presupuesto limitado.

¿Cuál es la principal ventaja? Que las conversiones de secuencia de secuencia a secuencia de objeto "solo funcionan".