Blog de Fernando Machado Piriz

Artículos sobre transformación digital, arquitectura empresarial y temas relacionados

La explicación fácil para entender covarianza y contravarianza

with 15 comments

Una de las novedades más importantes de C#4.0 y .NET Framework 4.0 es la introducción de covarianza y contravarianza. Varias veces he presentado covarianza y contravarianza en las charlas de novedades de C# y .NET Framework 4.0, y también en el blog, pero no siento que toda la audiencia termine entendiendo fácilmente estos conceptos. Hasta ahora he partido de las definiciones formales de covarianza y varianza, para luego mostrar cómo se implementan en C#. Probablemente no sea el mejor enfoque, así que ahora voy a intentar explicar estos conceptos de otra manera. Aquí vamos.

Vean las siguientes clases Animal y Cat que desciende de Animal:

class Animal { }
class Cat : Animal { }

Las siguientes declaraciones son correctas en cualquier versión de C#:

Cat kitty = new Cat();
Animal animal = kitty;

Todo Cat es un Animal (eso es lo que significa que Cat hereda de Animal), por lo tanto puedo asignar la variable kitty a la variable animal. Hasta ahora no hay nada nuevo.

Vean ahora lo que sucede cuando intento hacer algo análogo con enumerables de Animal y Cat:

IEnumerable<Cat> cats = new List<Cat>();
IEnumerable<Animal> animals = cats;

Todo Cat es un Animal, por lo que intuitivamente todo enumerable de Cat es un enumerable de Animal, ¿cierto? No, falso, al menos para los compiladores anteriores a C# 4.0, que dicen que no pueden convertir IEnumerable<Cat> en IEnumerable<Animal> y me preguntan si me estará faltando un cast.

clip_image001

Puedo agregar el cast, pero es un cast inseguro. Es decir, el programa compila, pero en tiempo de ejecución veo una excepción InvalidCastException en el momento de la asignación.

Ahora bien, coincidamos que a pesar que el compilador de C# no acepte que una todo enumerable de Cat es un enumerable de Animal, intuitivamente aceptamos la afirmación, en forma análoga a la afirmación todo Cat es un Animal.

C# 4.0 resuelve el conflicto. El fragmento de código anterior compila y no genera ninguna excepción en tiempo de ejecución, tal como nuestra intuición nos indica.

¿Por qué el mismo código que compila en C# 4.0 no compila en los anteriores?

Antes de C# 4.0 la interfaz IEnumerable estaba declarada como:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

Mientras que en C# 4.0 está declarada como:

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

Noten la palabra clave out junto al tipo parámetro T: sirve para indicar que la interfaz IEnumerable es covariante con respecto de T. En general, dado S<T>, la covarianza de S respecto de T implica que si la asignación de Y ← X es válida cuando X hereda de Y, entonces la asignación S<Y> ← S<X> también es válida.

Veamos ahora otro fragmento de código que involucra acciones (las acciones son delegados a funciones de la forma void Action<T>(T)) sobre Animal y Cat:

Action<Animal> doToAnimal =
    target => { Console.WriteLine(target.GetType()); };
Action<Cat> doToCat = doToAnimal;
doToCat(new Cat());

De nuevo, como todo Cat es un Animal, una Action<Animal> que puedo hacer con un Animal debería ser también una Action<Cat> que pueda hacer con un Cat. Vean que aunque la afirmación resulta razonable al decirla, los tipos parámetro están al revés que en el caso anterior: allá estaba asignando un enumerable definido en función de un Cat a un enumerable definido en función de un Animal, mientras que aquí estoy asignando una acción definida en función de Animal a una acción definida en función de Cat.

A pesar que la afirmación es razonable, a los compiladores anteriores a C# 4.0 no les gusta la asignación y arrojan al compilar un mensaje similar al del caso anterior: no se puede convertir una Action<Animal> en una Action<Cat>; en esta oportunidad no me pregunta si me está faltando un cast.

clip_image002

Nuevamente C# 4.0 resuelve el problema, y el código anterior sí compila. Veamos cómo están declaradas las acciones.

Antes de C# 4.0 el delegado Action estaba declarado como

public delegate void Action<T>(T obj)

Mientras que ahora en C# 4.0 está declarado así:

public delegate void Action<in T>(T obj)

Noten ahora la palabra clave in junto al tipo parámetro T: sirve para indicar que el delegado Action es contravariante con respecto de T. En general, dados S<T>, la contravarianza implica que si la asignación de Y ← X es válida cuando X hereda de Y, entonces la asignación S<X> ← S<Y> también es correcta.

En resumen, la covarianza en C# 4.0 permite que un método tenga un resultado de un tipo derivado del tipo definido como parámetro de un tipo genérico. De esta forma, permite asignaciones de tipos genéricos que siendo intuitivas, no eran posibles antes, tales como IEnumerable<Animal> IEnumerable<Cat>, cuando Cat hereda de Animal. Noten que IEnumerable sólo puede retornar instancias de T como resultado, no recibe instancias de T como parámetro. Por ello la palabra clave para declarar que IEnumerable es covariante respecto de T es out.

En forma análoga, la contravarianza permite que un método tenga argumentos de un tipo ancestro del tipo especificado como parámetro del tipo genérico. Así, permite también asignaciones que siendo razonables, no eran posibles, como por ejemplo Action<Cat>Action<Animal>. Vean que en este caso Action sólo puede recibir instancias, no las retorna. Por eso la palabra clave para declarar Action contravariante respecto de T es in.

En este artículo estoy mostrando un ejemplo de covarianza con una interfaz genérica y de contravarianza con un delegado genérico (con tipos parámetro); en un próximo artículo les mostraré otros ejemplos diferentes y les contaré que interfaces y delegados del .NET Framwork 4.0 son covariantes o contravariantes.

De aquí pueden descargar el código de ejemplo.

Para probarlo en compiladores anteriores a C# 4.0 y ver la diferencia, cambien el framework para el cual generan en las propiedades del proyecto:

clip_image003

Nos vemos en la próxima.

Written by fernandomachadopiriz

13/04/2010 a 08:42

15 comentarios

Subscribe to comments with RSS.

  1. Excelente Post!

    Alvaro Regalado

    13/04/2010 at 12:34

  2. […] intuitivamente aceptamos la afirmación, en forma análoga a la afirmación todo Cat es un Animal. Ver el articulo completo aqui. Saludos Fernando García Loera MVP Lead | Community Consultant | Latin American Region […]

  3. Muy claro, gracias

    Jaimir Guerrero

    16/04/2010 at 14:52

  4. Muy bueno.
    Algún otro día podías hablar de que los arrays son covariantes (histórica y discutida conveniencia) y de que las clases genéricas no son covariantes.

    Julio Fernández

    26/04/2010 at 16:15

  5. muy buen post, ameno la explicación

    muchas gracias

    johnny IV

    29/05/2010 at 14:23

  6. Gracias por la clara explicacion.!!

    LUI

    24/01/2013 at 10:21

  7. Gracias por la explicacion….ejemplos exactos, la parte de implicacion logica que usaste es muy bueno.

    aguavivadelaroca

    24/01/2013 at 10:23

  8. Perfectamente Claro.

    Cuitláhuac Togo

    04/12/2014 at 15:10


Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: