Covarianza y contravarianza en C#

La covarianza y la contravarianza son de esos conceptos de C# que no son fáciles de asimilar. De primeras parece algo fácil de entender, pero cuando te pones a ello te das cuenta de hay que pensarlo más detenidamente. Incluso muchas veces los estamos usando sin darnos cuenta.

En este artículo voy a tratar de explicar ambas propiedades. Espero que se entienda

Nota: estos conceptos provienen de una rama matemática llamada Teoría de categorías. En el blog de Tomas Petricek hay una explicación muy buena, sobre la teoría matemática y la relación con C#.

Clases derivadas, covarianza y contravarianza

La covarianza y contravarianza son propiedades que se atribuyen a jerarquías de clases. Por ejemplo podemos tener un clase Burger, de la cual heredan otras como KangreBurger y DobleKangreBurger. Simplificando podríamos decir que la clase Burger es mayor que las clases KangreBurger y DobleKangreBurger. Y como .NET toda clase hereda de Object podríamos decir que Object es mayor que Burger, Kangreburger o DobleKangreburger. Como digo es una simplificación para que nos entendamos, ya que no estamos comparando cuantitativamente las clases.

Hasta aquí la parte fácil.

Entrando en la parte difícil, podríamos decir que la covarianza es la propiedad por la cual podemos utilizar instancias de clases más pequeñas como si fueran una instancia de una clase mayor. Por ejemplo podríamos utilizar instancias de la clase KangreBurger, en lugar de especificar una instancia de la clase Burger.

IEnumerable<burger> = new List<kangreburger>;

El fragmento anterior funciona porque IEnumerable es covariante y acepta clases más pequeñas sin necesidad de conversiones.

La contravarianza, va en el sentido contrario. Podemos utilizar instancias de clases más grandes, como si fueran instancias de clases más pequeñas.

IComparer<kangreburger> comparador = new CustomComparer<burger>();

En este caso el fragmento funciona porque la interface IComparer es contravariante.

¿Y dónde se aplica la contravarianza y la covarianza?

Como hemos visto en los ejemplos anteriores, estas propiedades se pueden aplicar en interfaces genéricas, como IEnumerable o IComparer. Pero se pueden dar en más casos. Por ejemplo en Arrays:

Burger[] burgers = new KangreBurger[5];

Lo que hacemos en el ejemplo es totalmente válido, porque los Arrays en C# son covariantes. Aunque son covariantes con “peros”. El siguiente ejemplo provocaría un error en tiempo de ejecución:

Burger[] burgers = new KangreBurger[5];
burgers[0] = new DobleKangreBurger() { Name = "Doble Kangre Burger" };

El Array, debido a la covarianza, es un Array de KangreBurgers. Asignar una DobleKangreBurger a ese mismo Array provoca un error porque los tipos son diferentes. Esta comprobación se realiza en tiempo de ejecución, por lo que existe una penalización en el rendimiento. Eric Lippert explica que la decisión de hacer los Arrays covariantes fue polémica en su momento, e impuesta por la necesidad de hacer que C# se comportara como Java.

La covarianza también se puede dar en delegados. Por ejemplo:

Func<kangreburger> func = () => { return new KangreBurger(); };
Burger b = func();

La función func devuelve un objeto KangreBurger, que puede ser asignado a un Burger, sin tener que hacer ninguna conversión.

Un ejemplo de contravarianza lo podemos encontrar también en genéricos que reciben parámetros. Por ejemplo:

Action<burger> act = (newBurger) => { Console.WriteLine(newBurger.Name); };
Action<doblekangreburger> act2 = act;

En este caso creamos una función delegada que recibe un parámetro Burger. Y luego se lo asignamos a otra función que acepta parámetros de tipo DobleKangreBurger. Como Action es contravariante, la operación está permitida.

Si queréis una lista de interfaces y delegados que son covariantes o contravariantes que existen en C#, podéis encontrarla en este artículo de MSDN

Definiendo nuestras propias interfaces covariantes o contravariantes

Si queremos crear interfaces genéricas covariantes o contravariantes, tendremos que usar los parámetros de tipo con modificadores out o in según corresponda.

Interfaces covariantes

Por ejemplo supongamos las siguientes interfaces:

interface IPrintable<out t>
{
    IPropertyPrinter GetPrinter();
}

interface IPropertyPrinter 
{
    void PrintProperty<t>(T element, string propertyName);
}

Como se puede ver, IPrintable utiliza out, lo que quiere decir que es covariante. Podemos implementar las interfaces, en dos clases de la siguiente manera:

class Printer<t> : IPrintable<t>
{
    public IPropertyPrinter GetPrinter()
    {
        return new PropertyPrinter();
    }
}

class PropertyPrinter : IPropertyPrinter 
{

    public void PrintProperty<t>(T element, string propertyName)
    {
        var property = typeof(T).GetProperty(propertyName);
        Console.WriteLine(property.GetValue(element));
    }
}

Y como la interface es covariante, podremos hacer lo siguiente:

  var kangreBurger = new KangreBurger() { Name = "KangreBurger" };

  // Ejemplo de Covarianza
  // Al tener el out en IPrintable estamos permitiendo que Kangreburger actue como Burger
  IPrintable<burger> printer = new Printer<kangreburger>();
  var namePrinter = printer.GetPrinter();            
  namePrinter.PrintProperty<burger>(kangreBurger,"Name");

Quizá el ejemplo no sea el mejor, pero creo que se entiende. Como IPrintable es covariante, podemos utilizar objetos más pequeños sin recibir ningún error al realizar la conversión. Si quitáis el out de la interface, recibiréis un error diciendo que falta una conversión.

Interfaces contravariantes

Para este ejemplo utilizaremos las siguiente interface y su implementación:

interface IPropertyEditor<in t>
{
    void ChangeProperty(T element, string propertyName, object newValue);
}

class PropertyEditor<t> : IPropertyEditor<t>
{
    public void ChangeProperty(T element, string propertyName, object newValue)
    {
        var property = typeof(T).GetProperty(propertyName);
        property.SetValue(element,newValue);
    }
}

La contravarianza se aplica a parámetros de entrada. Aquí un ejemplo de su uso:

IPropertyEditor<kangreburger> editor = new PropertyEditor<burger>();
editor.ChangeProperty(kangreBurger, "Name", "KangreBurger con queso");
namePrinter.PrintProperty<burger>(kangreBurger, "Name");

En este caso utilizamos una clase Burger, pero como IPropertyEditor es contravariante no habrá problema. Si quitamos el in de la interface, entonces recibiremos un error de conversión no especificada.

Conclusiones

Tanto la covarianza, como la contravarianza son conceptos densos. Aunque los usamos a menudo sin darnos cuenta, no está de más conocerlas para poder saber que está pasando por detrás. O incluso utilizarlas en nuestras interfaces, sabiendo lo que estamos haciendo.



¿Quiéres que te avisemos cuando se publiquen nuevas entradas en el blog?

Suscríbete por correo electrónico o por RSS