C# Básico: Covarianza en genéricos

Muy buenas! Hacía tiempo que no escribía nada de la serie C# Básico. En esta serie voy tratando temas (sin ningún orden en particular) que considero que son fundamentos más o menos elementales del lenguaje. No es un tutorial al uso, cada post es independiente del resto y como digo no están ordenados por nada en particular.

El post de hoy nace a raíz de una pregunta que vi en los foros de msdn (http://social.msdn.microsoft.com/Forums/es-ES/vcses/thread/daf808ed-a0aa-4e1e-88ed-64ee60cce918), donde un usuario preguntaba porque el intentar convertir una List<LogVehiculos> a List<Log> le daba error teniendo en cuenta que LogVehiculos derivaba de Log.

Mi respuesta fue que en C# 3.5 los genéricos no son covariantes, y este post es para explicarlo todo un poco más 🙂

Antes que nada, covarianza y contravarianza son dos palabrejas muy molonas para explicar dos conceptos que son muy básicos pero que tienen implicaciones muy profundas. El mejor artículo en español que he leído sobre covarianza y contravarianza es el del Doctor (maestros hay algunos, doctores muchos menos) Miguel Katrib que salió publicado en la DotNetMania número 62 y titulado “La danza de las varianzas”. Es un artículo que debe leerse con atención pero sin duda de lo mejorcito que he leído nunca. Este post no entrará ni mucho menos en la profundidad de dicho artículo, así que si os interesa el tema, ya sabeis: haceros con dicha DotNetMania.

En este post nos vamos a centrar sólamente en la covarianza.

Covarianza

Llamamos covarianza a algo muy simple: Cuando permitimos sustituir un tipo D por otro tipo B. Para que eso sea posible debe cumplirse una condición: Que no haya nada que pueda hacerse con B y NO pueda hacerse con D.

Vamos a suponer que tenemos una clase Animal, de la cual deriva la clase Perro:

class Animal
{
public void Comer() { ... }
public void Dormir() { ... }
}

class Perro : Animal
{
public void VigilarCasa() { ... }
}

Si tenemos un método cualquiera que devuelva un perro, nosotros podemos convertir el resultado a un animal:

Perro ComprarPerro() { ... }
// entonces eso es válido:
Animal animal = ComprarPerro();

Eso es covarianza: el poder sustituir la clase derivada (Perro) que devuelve el método con la clase base (Animal). C# soporta covarianza entre una clase derivada y su clase base (como hacen de hecho todos los lenguajes orientados a objetos).

Tiene lógica, porque fijaos que no hay nada que pueda hacerse con un Animal (B) que no pueda hacerse con un Perro (D): Dado que Perro deriva de Animal hereda todos sus métodos y propiedades.

Pero la covarianza se da también en más casos y algunos de ellos están soportados en C#. Veamos…

Covarianza en delegados

Se trata de poder asignar a un delegado que devuelve un Animal un método que devuelve un Perro:

delegate Animal AnimalDelegate();
class Program
{
static Perro ObtenerPerro() { return new Perro(); }
static Animal ObtenerAnimal() { return new Animal(); }
static void Main(string[] args)
{
Animal animal = ObtenerPerro();
AnimalDelegate ad = new AnimalDelegate(ObtenerAnimal);
AnimalDelegate ad2 = new AnimalDelegate(ObtenerPerro);
}
}

Fijaos en la segunda declaración (ad2): Aunque el delegate está declarado para métodos que devuelven un Animal podemos usar este delegate con métodos que devuelvan un Perro. Por eso decimos que los delegates son covariantes en C#.

Covarianza en arrays

El siguiente código en C# funciona y es totalmente válido:

Animal[] animales = new Perro[100];

Es decir podemos asignar un array de Perros a un array de Animales. De nuevo los arrays son covariantes en C#. Esta decisión se tomó en su día para, bueno… luego hablaremos más sobre ella 🙂

Covarianza en genéricos

El siguiente código no compila en C#:

// error CS0029: Cannot implicitly convert type 'System.Collections.Generic.List<ConsoleApplication8.Perro>' to 'System.Collections.Generic.List<ConsoleApplication8.Animal>'
List<Animal> animales = new List<Perro>();

Es por ello que decimos que los genéricos NO son covariantes en C#.

Y ahora viene la pregunta… ¿por que?

Bien, recordad que si yo quiero sustituir un tipo D por otro tipo B eso significa que en un objeto de tipo D debo poder hacer cualquier cosa que haga en un objeto de tipo B. Es decir, si hay algo, llamémosle f(), que pueda hacer para un objeto de tipo B que no pueda hacer con un objeto de tipo D, no puedo aplicar covarianza… Ya que entonces podría hacer D.f() que no sería válido (recordad que f() es válido para B y no para D).

Cojamos el caso de List<Animal> y List<Perro> (recordad que Perro deriva de Animal). La pregunta es… hay alqo que podemos hacer con List<Animal> y que NO podamos hacer con List<Perro>? Veamos…

  • Con List<Animal> puedo contar cuantos animales hay. Con List<Perro> también.
  • Con List<Animal> puedo obtener todos los Animales que hay. Con List<Perro> puedo obtener los Perros, pero dado que Perro deriva de Animal, si obtengo un Perro estoy obteniendo un Animal (primer ejemplo que hemos visto). Así pues ningún problema.
  • Con List<Animal> puedo añadir un Animal. Con List<Perro> puedo añadir… un Perro. Ojo que eso es importante: A List<Animal> puedo añadirle cualquier Animal… puede ser un Perro, puede ser un Gato. A List<Perro> no puedo añadirle cualquier animal, debe ser un Perro forzosamente.

Por lo tanto ya hemos encontrado que se puede hacer con List<Animal> que no pueda hacerse con List<Perro>: Añadir un Gato.

Si C# nos dejara aplicar covarianza entonces eso sería válido:

List<Animal> animales = new List<Perro>();
animales.Add(new Gato()); // EEehhh... estoy añadiendo un Gato a una lista de Perros?

Por lo tanto, para evitar eso y asegurar que las listas de perros sólo tendrán perros el compilador no nos deja hacer esa conversión: Los genéricos no son covariantes.

Y los arrays? Recordáis que los arrays sí son covariantes. El siguiente código es válido y legal:

Animal[] animal = new Perro[100];
animal[0] = new Gato(); // Un Gato en una jauría de Perros!

Si ejecutas el siguiente código obtendrás una ArrayTypeMismatchException en tiempo de ejecución. Es decir el código compila pero luego rebienta.

Alguien podría decir que hubiesen aplicado eso mismo a las Listas… dejar que fuesen covariantes y luego rebentar en tiempo de ejecución si añado un Gato a una List<Perro>. Porque no lo han hecho así? Pues porque repetir errores no es nunca una buena solución. Los arrays jamás debieron haber sido covariantes. Si los crearon así fue para dar soporte a lenguajes tipo Java dentro del CLR (Java tiene arrays covariantes). Y así estamos: un error de diseño de Java propagado a .NET. Fijaos que eso obliga a que cada vez que añadimos un elemento en un array el CLR en tiempo de ejecución deba comprobar que el elemento realmente es del tipo del array. Viva la eficiencia!

Y con todo eso… llegó el Framework 4

Bien… Ahora analicemos el siguiente código:

static IEnumerable<Perro> JauriaDePerros()
{
return new List<Perro>();
}
static void Main(string[] args)
{
IEnumerable<Animal> perritos = JauriaDePerros();
}

Ya os lo avanzo: el siguiente código no compila con el Framework 3.5. Recordad: los genéricos no son covariantes y hemos visto la razón. Pero tiene sentido en este caso? Hay algo que pueda hacer con un IEnumerable<Animal> y que no pueda hacer con IEnumerable<Perro>? Veamos…

  1. Con un IEnumerable<Animal> puedo obtener todos los Animales. Con un IEnumerable<Perro> puedo obtener todos los Perros, pero como hemos visto ya, los Perros los puedo ver como Animales.

Y ya está. No puedo hacer nada más con un IEnumerable<> salvo obtener sus elementos. Entonces porque no compila el código en C#? Pues bien, porque pagan justos por pecadores: En el framework 3.5 los genéricos no son covariantes. Nunca, aunque por lógica pudiesen serlo.

Para tener una solución a estos casos donde la covarianza tiene sentido, debemos usar el Framework 4 (VS2010). Una de las novedades que incorpora C# en esta versión es precisamente esta: covarianza de genéricos en según que casos.

Veamos: la covarianza en genéricos es segura cuando el parámetro genérico se usa sólamente de salida. Es decir cuando ningún método acepta ningún parámetro del tipo genérico, como mucho sólo lo devuelven. El problema en el caso de List<> estaba en que podía añadir un Gato a una lista de Perros. Y eso es posible porque uno de los métodos de la clase List<T> es Add(T item). Es decir el tipo genérico se usa como valor de entrada a los métodos. En cambio con IEnumerable<T> hemos visto que no hay ningún problema: En un IEnumerable<T> sólo puedo obtener sus elementos, pero no puedo añadirle elementos nuevos. No hay ningún método que reciba un parámetro del tipo genérico. Como mucho hay métodos que devuelven ojetos del tipo genérico. En este caso la covarianza es segura.

Para indicar en C# 4.0 que una clase genérica es covariante respecto a su tipo genérico, usamos la palabra clave out. P.ej. IEnumerable<T> en C# 4.0 está definido como:

public interface IEnumerable<out T> : IEnumerable
{
// Métodos...
}

Fijaos en el uso de out para indicarle al compilador: Este tipo es covariante respecto al tipo genérico T. Entonces este código que en VS2008 no compilaba, es válido en C# 4.0:

static IEnumerable<Perro> JauriaDePerros()
{
return new List<Perro>();
}
static void Main(string[] args)
{
IEnumerable<Animal> perritos = JauriaDePerros();
}

Por supuesto, esto sigue sin compilar:

static List<Perro> JauriaDePerros()
{
return new List<Perro>();
}
static void Main(string[] args)
{
List<Animal> perritos = JauriaDePerros();
}

Ya que List<T> no es covariante respecto al tipo genérico T (lógico, si lo fuese podría añadir un Gato a una lista de Perros).

En cambio eso si que es correcto en VS2010:

static List<Perro> JauriaDePerros()
{
return new List<Perro>();
}
static void Main(string[] args)
{
IEnumerable<Animal> perritos = JauriaDePerros();
}

Aunque el método JauriaDePerros() devuelve una List<Perro>, el código funciona porque:

  1. List<T> implementa IEnumerable<T>
  2. IEnumerable<T> es covariante respecto a T

En el fondo, fijaos que no hay problema: con perritos lo único que puede hacerse es obtener sus elementos, así que de nuevo no hay peligro de que añada un Gato a perritos.

Declaración de mis clases genéricas covariantes

Si yo creo una clase que quiera que sea covariante con su tipo genérico, simplemente debo usar out. La única restricción es que ningún método de mi clase podrá aceptar un parámetro del tipo genérico:

interface Foo<out T>
{
T Bar() { ... }
void Baz(T t) { ... }
}

Este código no compila (error CS1961: Invalid variance: The type parameter ‘T’ must be contravariantly valid on ‘ConsoleApplication5.Foo<T>.Baz(T)’. ‘T’ is covariant.). Ese mensaje de error largote lo único que quiere decir es que T es covariante, y por lo tanto no podemos aceptar parámetros de tipo T.

Finalmente tened presente que sólo las interfaces pueden declarar que su tipo genérico es covariante (las clases no).

Bueno… dejémoslo aquí. Hay otro termino ligado a la covarianza que es la contravarianza, aunque no es tan común como la covarianza y quizá algún día hablemos de ella 🙂

Un saludo y recordaros lo que digo siempre en los posts de esta serie: Si tenéis temas sobre el lenguaje C# que queráis tratar, hacédmelo saber y haré lo que pueda!!!

13 comentarios sobre “C# Básico: Covarianza en genéricos”

  1. Cacho artículo te has currado tío!
    Muy guapo (el artículo, eh?) 🙂

    Menudo elemento el Dr. Katrib! Tuve la oportunidad de compartir mesa en con él en dos ocasiones en 2004. Además de un genio, contó un par de chistes de ‘escojonarse’… 😀

  2. @A todos
    Muchas gracias por vuestros comentarios! 🙂

    @Lluis
    Yo no he tenido el placer de conocer al Doctor Katrib, pero la verdad por lo que he leído de él y lo que me han contado es alguien a quien me encantaría tener la oportunidad de conocerlo y hablar con él 🙂

  3. @superdeibi
    Gracias por la corrección 😉

    Tengo tan interiorizado que «reventar» va con b debido a la influencia del catalán, que me pasa esto… :p

    Un saludo!

  4. Ijoles!!! que explicacion. Mis respetos, te he leido en dos articulos (el otro fue sobre Inyeccion de dependiencias) y he quedado impresionado… Te seguire leyendo!!

  5. @Balam
    Yo más bien diría que polimorfismo es una consecuencia de la covarianza.

    Covarianza es el concepto de poder sustituir un tipo por otro, mientras que polimorfismo es la capacidad que tiene el tipo sustituyente de modificar el comportamiento del tipo sustituído (modificar, no añadir ni quitar).

    Pero puedes tener covarianza sin polimorfismo: P.ej. double es covariante con float pero no existe polimorfismo entre double y float!

    Saludos!

Responder a etomas Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *