Saltar a contenido

A2.B Restricciones y variación

Restringir el tipo

Dentro de una función genérica, T es «cualquier tipo», y de «cualquier tipo» no se sabe nada: no se puede compararlo con <, ni sumarlo, ni crearlo con new T(). Una restricción (where) dice qué tiene que cumplir T, y a cambio permite usar esas capacidades:

Restricción Significa Permite
where T : IComparable<T> T se puede comparar con otro T a.CompareTo(b)
where T : class / struct T es un tipo de referencia / de valor Comparar con null / usar T? como Nullable
where T : new() T tiene constructor sin parámetros new T()
where T : notnull T no admite null Usarlo como clave de un diccionario
where T : MiInterfaz T cumple una interfaz (o hereda de una clase) Llamar a sus miembros
where T : INumber<T> T es un número (C# 11) Operadores +, *... y T.Zero
Console.WriteLine(Mayor(3, 9));
Console.WriteLine(Mayor("pera", "casa"));
Console.WriteLine(Mayor(2.5, 1.5));

// Aritmética genérica (C# 11): cualquier tipo numérico
Console.WriteLine(SumaTodos([1, 2, 3]));
Console.WriteLine(SumaTodos([1.5, 2.5]));

// Mayor<T> necesita poder comparar
T Mayor<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;

// INumber<T> da operadores como + y el valor T.Zero
T SumaTodos<T>(IEnumerable<T> elementos) where T : System.Numerics.INumber<T>
{
    T total = T.Zero;
    foreach (T x in elementos)
    {
        total += x;
    }
    return total;
}

Salida:

9
pera
2,5
6
4

Mayor solo compila con tipos que sepan compararse (int, string, double...). Si lo llamas con un tipo propio que no implementa IComparable<T>, el compilador lo rechaza antes de ejecutar. La última, INumber<T>, es una novedad de C# 11 («matemática genérica») y no tiene equivalente directo en Java, Kotlin ni Dart: permite escribir una sola SumaTodos para int, double, decimal...

¿Una lista de enteros es una lista de números?

Parece que si string es un object, entonces una List<string> debería poder usarse como List<object>. No puede, y es una protección: si lo permitiera, podrías añadir un 42 a la lista de textos a través de la referencia List<object>.

La variación dice cuándo sí es seguro:

Tipo Variación Qué permite
List<T> Invariante Solo List<string> con List<string>
IEnumerable<out T> Covariante (out) Un IEnumerable<string> donde se pide IEnumerable<object>: solo se sacan valores
Action<in T> Contravariante (in) Un Action<object> donde se pide Action<string>: solo se meten valores

Una regla para recordarlo: lo que solo produce valores (out) puede ser más específico; lo que solo consume (in) puede ser más general; lo que hace las dos cosas (List<T>) no puede variar.

string[] textos = ["a", "b"];
object[] objetos = textos;           // los arrays son covariantes... (permitido)
try
{
    objetos[0] = 42;                 // ...pero esto falla al EJECUTAR
}
catch (ArrayTypeMismatchException)
{
    Console.WriteLine("array: ArrayTypeMismatchException");
}

List<string> listaTextos = ["a", "b"];
// List<object> listaObjetos = listaTextos;      // error CS0029: no compila (List<T> es invariante)

IEnumerable<string> secuenciaTextos = listaTextos;
IEnumerable<object> secuenciaObjetos = secuenciaTextos;      // IEnumerable<out T> es covariante: sí compila
Console.WriteLine($"IEnumerable<object> con {secuenciaObjetos.Count()} elementos");

Action<object> mostrarObjeto = o => Console.WriteLine($"objeto: {o}");
Action<string> mostrarTexto = mostrarObjeto;                 // Action<in T> es contravariante: sí compila
mostrarTexto("hola");

Salida:

array: ArrayTypeMismatchException
IEnumerable<object> con 2 elementos
objeto: hola

Los arrays son un caso especial y peligroso: C# permite tratar un string[] como object[] (por compatibilidad con los inicios del lenguaje), pero al meter un valor que no es del tipo real el programa falla al ejecutar. Es otra razón para preferir List<T>.

Errores frecuentes

Error Cómo evitarlo
Comparar con < valores de tipo T Restringir con where T : IComparable<T> (o INumber<T> para números)
Pasar una List<string> donde se pide List<object> Pedir IEnumerable<object> (solo lee) o hacer la función genérica
Usar new T() sin restricción where T : new()
Usar T como clave de diccionario sin notnull where T : notnull
Fiarse de la covarianza de los arrays Usar List<T>

Para practicar

Los ejercicios A2.3 y A2.5 usan restricciones y funciones como parámetro. Para ver la misma idea escrita en los otros lenguajes: Pasar de uno a otro.