Covariance & Contravariance en C#

Antes de hacer el video indicar que he buscado el término en español, y no existe, únicamente existe su versión en Inglés así que con ella seguiremos. 

Lo primero que vamos a ver es la estructura de objetos, o el modelo que vamos a necesitar para entender y comprender el post, ya que es algo complejo sin una estructura clara:

public class Persona
{
    public string Name { get;set;}
}

public class Empleado : Persona
{
    public int Id { get; set; }
}

public class Jefe : Empleado
{
}

public class Becario : Empleado
{
}

Este es el resultado que se nos queda al final con las relaciones de entidades:

ejemplo modelo

Como puedes observar, hay que tener muy claro la programación orientada a objetos y dentro de ella, la herencia, si no lo tienes claro, aquí hay un enlace.

Nuestro caso en particular, tiene también mucho que ver con los tipos genéricos, para el que también existe un post. 

 

1 - Invariance

Antes de pasar a explicar covariance y contravariance, explicaremos el caso por defecto.

Como recordamos del post de generics elementos como la List recibe T  como tipo genérico, esto quiere decir que no contiene las palabras clave o decoradores in u out

como podemos ver en el ejemplo creamos un método que recibe una lista Persona e imprime el tipo de las mismas:

public static void ImprimirPersonas(List<Persona> personas)
{
    foreach (var persona in personas)
    {
        Console.WriteLine($"El elemento actual es de tipo {persona.GetType().ToString()}");
    }
}

Y este código nos acepta todas las entidades que heredan de Persona:

var listaPersonas = new List<Persona>()
{
    new Becario(), 
    new Jefe()
};

Utils.ImprimirPersonas(listaPersonas);

Sin embargo, pasar una colección de un tipo que hereda de nuestro tipo original Persona nos da un error:

var listaBecarios = new List<Becario>()
{
    new Becario(),
    new Becario()
};

Utils.ImprimirPersonas(listaBecarios);

Y el error que nos da es que no puede convertir de List<Persona> a List<Becario>

Hasta aquí bien, esto ya lo habíamos visto anteriormente. Pero, por qué? 

El motivo es que el tipo genérico de List<T> no es covariant. Como indicamos List<T> es invariable (invariant) por lo que únicamente acepta colecciones donde el tipo T debe ser Persona

El motivo por el que el compilador lo prevé es para evitar porder hacer lo siguiente: 

public static void ImprimirPersonas(List<Persona> personas)
{
    personas.Add(new Becario());
}

Como vemos es la misma cabecera para el método, pero su contenido es completamente erróneo, estamos añadiendo a la lista un elemento y podría violar cualquier otra condición de restricción de los métodos o los tipos. recuerda que vimos como restringir las condiciones en el post sobre los tipos genéricos. 

 

2 - Covariance

Utilizamos covariance normalmente cuando creamos colecciones inmutables. Con estas colecciones prevenimos el poder añadir o quitar elementos a dichas colecciones.

Para indicar que es covariant, debemos añadir la palabra clave out en el tipo genérico. 

En este ejemplo voy a utilizar IEnumerable<out T> que es un tipo predefinido de .NET muy similar a las listas pero que como vemos, contiene la palabra clave out.

Por ello creamos el siguiente método:

public static void ImprimirPersonas(IEnumerable<Persona> personas)
{
    foreach (var persona in personas)
    {
        Console.WriteLine($"El elemento actual es de tipo {persona.GetType().ToString()}");
    }
}

Vemos que es prácticamente igual que el anterior, únicamente cambiamos el tipo que pasamos al método.

Al ser un tipo que permite tipos genéricos y covariant, quiere decir que podemos pasar una lista de cualquier tipo que herede de nuestra lista principal.

en este caso en concreto, podemos pasar una lista donde el tipo genérico puede ser Persona, Becario, Empleado, o Jefe

si volvemos al ejemplo anterior: 

var listaBecarios = new List<Becario>()
{
    new Becario(),
    new Becario()
};

Utils.ImprimirPersonas(listaBecarios);

Vemos que ya no nos da error. 

 

3 - Contravariance 

Utilizamos contravariance comunmente cuando pasamos funciones como parametros, y debemos indicar el tipo genérico con la palabra clave in. en C# un tipo que viene por defecto que acepta in es Action<in T> el cual es un delegado.

Podemos crear un método que reciba por parámetro un Action<Becario> y que invoque una nueva instancia de nuestro Becario.

public static void RealizarActionBecario(Action<Becario> becarioAction)
{
    Becario becario = new Becario();
    becarioAction(becario);
}

Escribimos el codigo correspondiente para ejecutar dicho método y vemos que funciona correctamente:

var accionBecario = new Action<Becario>(z => Console.WriteLine("Preparo el café"));
Utils.RealizarActionBecario(accionBecario);

Sin embargo, el siguiente código falla:

var accionJefe = new Action<Jefe>(z => Console.WriteLine("Organizo un meeting"));
Utils.RealizarActionBecario(accionJefe);

Y es debido a que no puede convertir de Action<Becario> a Action<Jefe>.

Pero aquí es donde nuestra decorador in entra en juego, si ejecutamos el siguiente código:

var accionEmpleado = new Action<Empleado>(z => Console.WriteLine("trabajo duro"));
Utils.RealizarActionBecario(accionEmpleado);

Funciona perfectamente, y esto es debido a que utilizando la palabra reservada in, permitimos que su clase, y las clases de las que hereda, en este caso Empleado y Persona funcionen correctamente.

 

Conclusión

  • El uso de Covariance and Contravariance está altamente ligado a los tipos genéricos.
  • Invariance: permite solo del tipo especificado, in palabra reservada.
  • covariance: palabra reservada out; Permite de un tipo o de los que heredan de él.
  • contravariance: palabra reservada in; permite un tipo o los tipos que implementan, o tipos padre. 
  • Entender su funcionamiento es importante.
  • Nos ayuda a tener un código más robusto y estructurado.