Hace un tiempo, mientras programaba sobre un proyecto que no era mío, noté que en varias ocasiones la persona que lo había hecho declaraba interfaces utilizando “IMyInterface< out T >” o “IMyInterface< in T >“. Como era la primera vez que veía una declaración así, se me ocurrió investigar pero encontré muy pocos lugares en donde la explicación fuera realmente clara. Así que me puse a hacer pruebas, a investigar más a fondo y terminé entendiendo cuál era su funcionamiento. Debido a esos problemas, decidí escribir acerca de esto llamado: Covarianza y Contravarianza.
Antes de arrancar, para explicar esto voy a usar una aplicación de consola. Podemos usar cualquier compilador como Visual Studio, Rider, etc, o, lo que voy a usar, un compilador online COMO ESTE:
Empecemos con algunas pequeñas cosillas. Imaginemos un ejemplo básico de herencia. Una clase padre y dos hijas:
public class Animal { } public class Cat : Animal { } public class Dog : Animal { }
Ahora… Si yo tuviera una variable de tipo “Animal” podría guardar dentro de ella instancias de tipo “Animal“, “Gato” o “Perro” (o cualquier instancia de clase que herede de “Animal“). Algo tan simple como esto:
Animal myAnimal = new Cat();
Hasta el momento, nada complicado… Ahora… Empecemos con algo un toque más grande. Imaginemos que usamos a estos animales dentro de una interfaz, como valor genérico. Probemos en una aplicación de consola simple.
public class Program { public static void Main() { IMyInterface< Animal > myAnimal = new GenericAnimal< Cat >(); } } public interface IMyInterface< T > { } public class Animal { } public class Cat : Animal { } public class Dog : Animal { } public class GenericAnimal< T > : IMyInterface< T >{}
Lo que vamos a notar es que el compilador va a tirarnos un error, diciéndo que no podemos convertir GenericAnimal< Cat >
en IMyInterface< Cat >
. Pero… Por qué? Si Cat
hereda de Animal
?
A simple vista pareciera que esto debería funcionar y que no tiene sentido que tire error. La realidad es que el compilador sabe que Cat
hereda de Animal
, el problema es que no le estamos asegurando que todo lo que hagamos dentro de IMyInterface
con un T
como Cat
lo vamos a poder hacer con un T
como Animal
. Suena complicado, no? Vamos a explicarlo de otra manera. Imaginemos este otro escenario:
using System.Collections.Generic; public class Program { public static void Main() { List< Animal > myAnimals = new List< Cat >(); } } public interface IMyInterface< T > { } public class Animal { } public class Cat : Animal { } public class Dog : Animal { } public class GenericAnimal< T > : IMyInterface< T >{}
Noten que el compilador tira exactamente el mismo error. Por qué?… Básicamente… El compilador diría algo como:
- COMPILADOR:
Querido programador… Si yo te permito hacer eso, entonces la INSTANCIA de la lista que estás metiendo va a ser ESPECÍFICAMENTE de gatos, mientras la variable es de ANIMALES. Entonces… Qué tendría que hacer si después me terminas agregando un PERRO a esa lista? Por tu bien, no te dejo hacerlo. :D.
Y yo podría responderle algo como:
PROGRAMADOR:
Querido compilador: No importa! Mi variable es una lista de animales… Y un perro es un animal.
A lo cual el compilador, muy cortésmente, respondería:
COMPILADOR:
Sí… PERO… Aunque tu VARIABLE represente una lista de ANIMALES, la instancia que pusiste arriba es de GATOS, y sólo podrías agregar GATOS a dicha lista. Pero como la variable es de ANIMALES yo te debería dejar agregar algo como PERROS. En tal caso, me romperías cuando hagas eso. Entonces, para que no la cagues, no te dejo hacerlo.

Entonces… Para evitar que el proyecto se rompa en tiempo de ejecución, el compilador prefiere evitarlo diciéndote “eso no se puede, cambialo o no compilo. Hum“. Viéndolo de esa manera, quizás el planteo del compilador tiene algo de razón y nos está previniendo de un posible error. Pero el problema del compilador pasa por la seguridad: el compilador no está seguro de que vayamos a hacer lo correcto. Pero… Qué pasa si se lo aseguramos?
Es acá donde entra todo este tema de la “covarianza” y “contravarianza“. Empecemos por el primer concepto:
COVARIANZA:
QUERIDO COMPILADOR:
“Te aseguro que todas las acciones que vaya a realizar con una subclase, las vas a poder realizar también con la clase base”.
Con amor, el programador.
Este concepto se refiere básicamente a avisarle que si meto un GATO (subclase), o un PERRO (subclase), dentro de una lista o cualquier cosa, cuyo valor genérico sea ANIMAL (clase base), todo lo que haga con ellos va a poder hacerse también con una animal, como por ejemplo, CAMINAR. O sea que el compilador sabe que jamás voy a hacer cosas como hacer ladrar a un gato.
Para hacerlo, tengo que declarar la interfaz de la siguiente manera:
public interface IMyInterface< out T > { }
Pero el compilador, que no es tonto, se previene también de esto. Por eso, no va a dejar que declares ninguna property o método, que RECIBA como parámetro al valor de tipo T. O sea… Dentro de esa interfaz sólo puedo SACAR COSAS (de ahí el “out“), pero no puedo INGRESAR nada. Si intentamos crear una property o método que intente METER un valor dentro de la clase, el compilador nos va a tirar un error, qué más o menos va a significar:
QUERIDO USUARIO:
No me dijiste que todo lo que ibas a hacer con los animales que metas, es lo mismo que podría hacer con un ANIMAL? Eso quiere decir que no vas a intentar meter un PERRO o un GATO u otro animal… Así que voy a asumir que las cosas que están adentro ya son de un tipo de dato T. No necesitas meter nada. Besitos.
De esta manera, el compilador asume que todo lo que esté adentro de esa clase, usado como valor T, sin importar de qué tipo sea, asume que todas las acciones que vamos a realizar van a ser propias del tipo de dato T que hayamos escogido. Por ende, al accederlo nos puede recomendar tranquilamente variables y métodos propias del tipo de dato T, porque asume que va a ser ÚNICO. No es que yo le digo que va a ser de ANIMAL y después le termino metiendo animales diferentes. No. Asume que eso no lo vamos a hacer, por ende nos bloquea la posibilidad de que lo hagamos. Así que cuando accedamos al tipo T (en el caso de que sea ANIMAL), nos va a sugerir cosas propias de la clase ANIMAL, o del tipo de dato que hayamos escogido para el valor T. Es como si dijera: “No necesitas que te muestre otra cosa… Total… Todo lo que podes hacer con cualquier tipo de animal (o una subclase de T), lo vas a poder hacer con un animal genérico (clase base T).
Entonces… Sólo puedo OBTENER datos, pero no puedo AGREGAR o INGRESAR ninguno. A esto se le dice COVARIANZA. EJ:
public class Program { public static void Main() { IMyInterface< Animal > myAnimals = new GenericAnimal< Cat >(); } } public interface IMyInterface< out T >{} public class Animal { } public class Cat : Animal { } public class Dog : Animal { } public class GenericAnimal< T > : IMyInterface< T >{}
Estos serían dos ejemplos de uso, uno correcto y uno incorrecto, como para que vean un uso:
<< MODO CORRECTO >>
public class Program { public static void Main() { IMyInterface< Animal > myAnimals = new GenericAnimal< Cat >(); } } public interface IMyInterface< out T > { T Get(); } public class Animal { } public class Cat : Animal { } public class Dog : Animal { } public class GenericAnimal< T > : IMyInterface< T > { public T Get() { //Acá podría devolver una instancia de T. return default(T); } }
<< MODO INCORRECTO >>
public class Program { public static void Main() { IMyInterface< Animal > myAnimals = new GenericAnimal< Cat >(); } } public interface IMyInterface< out T > { T Get(); void Add(T another); //ERROR!! } public class Animal { } public class Cat : Animal { } public class Dog : Animal { } public class GenericAnimal< T > : IMyInterface< T > { public T Get() { //Acá podría devolver una instancia de T. return default(T); } public void Add(T another) { } }
El segundo bloque de código tiraría un error, avisando que estás usando T como una CONTRAVARIANTE, porque estamos intentando pasar un parámetro, cuando al mismo tiempo le ASEGURAMOS (con el out
que pusimos en la interfaz), que no vamos a hacerlo. Pero… Qué es una CONTRAVARIANTE?
CONTRAVARIANZA:
Si entendieron bien el uso de las COVARIANTES, entonces no hay mucho que decir de esto más que: “es lo mismo, pero al revés“. En este caso, le vamos a decir al compilador que yo podría necesitar crear una GenericAnimal
con un valor T
que sea Cat
, pero que puedo meterle adentro otro tipo de animal, como un perro.

En otras palabras… Es como decirle al compilador:
QUERIDO COMPILADOR:
“Quiero crear algo cuyo valor genérico T va a ser de un tipo específico, y probablemente también quiera meter datos que son clases base de T. Pero… TE ASEGURO… Que jamás voy a hacer algo con una clase base de T, que no pueda hacer con la clase T”.
Con cariñito… El programador.
Entonces… Básicamente le estoy diciendo que, por ejemplo, si yo hago que el valor T
sea un Cat
, y por alguna razón se me ocurre meter ahí adentro un perro u otro animal, no voy a hacer con ese perro (u ese otro animal), algo que no pueda hacer con el Cat
. Para hacerlo, escribo lo siguiente:
public interface< in T > IMyInterface { }
Ahora… Si yo intento crear una función dentro de la interfaz, que devuelva el tipo T
, el compilador tira un error, ya que con esto le digo que SOLAMENTE voy a meter cosas (animales, por ejemplo), pero no voy a querer exponerlos de ninguna manera, por ende, desde afuera no los voy a necesitar acceder. Y si declarara algo que DEVUELVE un valor T
, el compilador tiraría un error, qué básicamente nos estaría diciéndo:
QUERIDO USUARIO:
Me dijiste que todo lo que hagas con una clase base de T
, también lo vas a hacer con T
. Por ende, jamás vas a querer dejar nada a la vista, porque no vas a querer sacarlo de ahí adentro. De eso se debería encargar la clase que implementa esta interfaz, y no alguien ajeno.
Con amor… Tu compilador favorito. <3.
Si el compilador no nos tirara ese error, nosotros podríamos hacer algo como esto:
public class Program { public static void Main() { IMyInterface< Cat > myAnimals = new GenericAnimal< Animal >(); } } public interface IMyInterface< in T > { T Get(); } public class Animal { } public class Cat : Animal { } public class Dog : Animal{ } public class GenericAnimal< T > : IMyInterface< T > { public T Get() { return default(T); } }
O sea que podríamos meter un perro y luego querer obtenerlo para que ladre, pero arriba le dijimos que el valor T era de gatos. Hacerlo implicaría que un gato podría ladrar. Por ende, el compilador se asegura de que no hagas eso, prohibiéndolo en tiempo de compilación directamente. A esto se lo llama CONTRAVARIANZA. Y se refiere a que sólo voy a ingresar cosas, pero jamás voy a intentar usarlas desde afuera.
Pasando todo en limpio quedaría así:
<< MODO CORRECTO >>
using System; public class Program { public static void Main() { GenericAnimal< Animal > zoo = new GenericAnimal< Animal >(); zoo.Add( new Cat() ); zoo.Add( new Dog() ); //Aunque sean listas de gatos... //... Les puedo guardar listas de animales. IMyInterface< Cat > myCats = zoo; IMyInterface< Dog > myDogs = zoo; } } public interface IMyInterface< in T > { void Add(T another); } public class Animal { } public class Cat : Animal { } public class Dog : Animal { public void Bark() { Console.WriteLine("WOW!"); } } public class GenericAnimal< T > : IMyInterface< T > { public void Add(T another) { } }
<< MODO INCORRECTO >>
public class Program { public static void Main() { IMyInterface< Cat > myAnimals = new GenericAnimal< Animal >(); myAnimals.Get().Bark(); } } public interface IMyInterface< in T > { T Get(); } public class Animal { } public class Cat : Animal { } public class Dog : Animal { public void Bark() { Console.WriteLine("WOW!"); } } public class GenericAnimal< T > : IMyInterface< T > { public T Get() { return default(T); } }
Para este último caso, garantizar por medio del operador in
al programa que todo lo que vamos a hacer con un objeto lo vamos a poder hacer con cualquiera nos permite también a nosotros evitar, más que nada cuando trabajamos en grupo, que se ingresen a la clase objetos que podrían romper el esquema.