La varianza es la regla que decide si un tipo genérico puede sustituirse por otro relacionado por herencia.
Si llevas un tiempo programando, seguramente te has encontrado con situaciones extrañas donde el compilador te grita “No se puede convertir List<Perro> a List<Animal>”.
Y tú piensas:
“Pero vamos a ver… si un
Perroes unAnimal,¿por qué una lista de perros no es una lista de animales?”
La respuesta corta es: por seguridad de tipos. La respuesta larga tiene que ver con tres conceptos clave: Invarianza, Covarianza y Contravarianza.
Estos términos, que parecen sacados de un libro de cálculo avanzado, simplemente describen cómo se comportan los tipos complejos (como listas, arrays o delegados) cuando intentamos sustituirlos por tipos derivados.
Vamos a intentar verlo, sin jugarnos una aneurisma 👇.
El escenario base
Para todos los ejemplos, vamos a usar esta jerarquía clásica. Es simple, pero efectiva:
public class Animal { }
public class Perro : Animal { }
public class Gato : Animal { }
Sabemos, por herencia, que esto es válido:
Animal miMascota = new Perro(); // ✅ Correcto. Un perro es un animal.
Pero “la liada” viene cuando envolvemos estos tipos en “contenedores” o genéricos.
Invarianza: El comportamiento por defecto
Por defecto, los genéricos son Invariantes. Esto significa que List<Perro> NO tiene ninguna relación con List<Animal>. Son tipos totalmente distintos para el compilador.
¿Por qué? Supongamos que el compilador nos dejara hacer esto:
// ❌ Esto NO compila (y menos mal)
List<Perro> perros = new List<Perro>();
List<Animal> animales = perros; // Supongamos que esto fuera posible
animales.Add(new Gato()); // 🙀 ¡Desastre!
Si pudiéramos tratar la lista de perros como una lista de animales, podríamos meter un Gato dentro.
Como la variable original perros sigue apuntando a la misma lista en memoria, de repente tendríamos un Gato dentro de una List<Perro>. Cuando intentáramos leerlo como perro… ¡Boom! Error en tiempo de ejecución 💥.
La Invarianza nos protege de corromper la memoria mezclando tipos incompatibles. Si el tipo genérico permite Entrada y Salida de datos (como una List<T>), debe ser invariante.
Covarianza: “Fluye con la herencia”
La Covarianza permite que Wrapper<Perro> sea tratado como Wrapper<Animal>, siempre que ese tipo genérico esté diseñado para ello.
El flujo de la asignación va en la misma dirección que la herencia.
Perro->AnimalIEnumerable<Perro>->IEnumerable<Animal>
¿Cuándo es seguro esto? Cuando el contenedor SOLO nos permite SACAR datos (Output), pero no meterlos.
En C# (y otros lenguajes), esto se suele marcar en las interfaces con la palabra clave out. El ejemplo perfecto es IEnumerable<out T>.
// ✅ Covarianza permitida
IEnumerable<Perro> perros = new List<Perro>();
IEnumerable<Animal> animales = perros;
Como IEnumerable es de “solo lectura” (no tiene un método Add), no hay riesgo de que metamos un Gato. Solo podemos iterar y obtener animales. Y como todo perro que saquemos es un animal, es una operación segura.
Idea práctica: usamos covarianza cuando el tipo genérico se usa solo como valor de retorno.
Contravarianza: “Fluye contra la herencia”
Aquí es donde la cabeza nos explota un poco. La Contravarianza permite lo opuesto en tipos de entrada: usar algo que acepta Animal donde se espera algo que acepta Perro.
El flujo va en dirección contraria a la herencia.
Animal<-PerroAction<Animal>->Action<Perro>
¿Cómo es esto posible? Esto ocurre cuando el contenedor SOLO nos permite METER datos (Input).
Supongamos un delegado Action<T> (una función que recibe un parámetro y no devuelve nada). Tenemos una función que sabe “acariciar animales”.
void AcariciarAnimal(Animal a) { /* ... */ }
Si yo tengo un delegado que espera saber “Acariciar Perros”, ¿le puedo pasar mi función genérica?
// ✅ Contravarianza permitida
Action<Animal> expertoEnAnimales = AcariciarAnimal;
Action<Perro> cuidadorDePerros = expertoEnAnimales;
cuidadorDePerros(new Perro());
¡Sí! Porque si el expertoEnAnimales sabe tratar con cualquier animal, entonces por definición sabe tratar con un perro.
Lo que NO podríamos hacer es pasarle un Action<Perro> a alguien que espera un Action<Animal>, porque si le pasamos un Gato, el cuidador de perros no sabría qué hacer.
En C#, esto se marca con la palabra clave in (ej: IComparer<in T>).
Idea práctica: usamos contravarianza cuando el tipo genérico se usa solo como argumento de entrada.
Resumen rápido
Para no liarnos, aquí tenéis la “chuleta”:
| Concepto | Dirección | Palabra Clave (C#) | Uso Principal | Ejemplo |
|---|---|---|---|---|
| Invarianza | Ninguna | (ninguna) | Lectura y Escritura | List<T> |
| Covarianza | Hijo -> Padre | out | Solo Lectura (Salida) | IEnumerable<T>, Func<T> |
| Contravarianza | Padre -> Hijo | in | Solo Escritura (Entrada) | Action<T>, IComparer<T> |
