A2.A Tipos parametrizados¶
Para quién es esto
Es material avanzado: da por sabidas las unidades 1 a 5, en especial clases, interfaces y colecciones.
Por qué existe¶
Imagina una caja que guarda un valor de cualquier tipo. Sin genéricos hay dos malas opciones: escribir una clase por cada tipo (CajaInt, CajaTexto...) o guardar object y convertir al sacar el valor, perdiendo la comprobación de tipos. Los tipos parametrizados (genéricos) resuelven esto: escribes la clase una vez, con un parámetro de tipo (T) que se concreta al usarla, y el compilador sigue comprobando todo.
Mira lo que pasa con la colección antigua ArrayList (que guarda object) frente a la genérica List<int>:
using System.Collections;
// ArrayList (de antes de los genéricos) guarda cualquier cosa como object
var vieja = new ArrayList { 1, 2, "tres" };
try
{
int suma = 0;
foreach (object o in vieja)
{
suma += (int)o; // el compilador no avisa: falla al ejecutar
}
}
catch (InvalidCastException)
{
Console.WriteLine("ArrayList: InvalidCastException al ejecutar");
}
// List<int> solo admite int: el error se detecta al compilar
var nueva = new List<int> { 1, 2 };
// nueva.Add("tres"); // error CS1503: no se puede convertir de string a int
Console.WriteLine($"List<int>: suma {nueva.Sum()}");
Salida:
ArrayList: InvalidCastException al ejecutar
List<int>: suma 3
La sintaxis en C¶
- Una clase genérica declara su parámetro entre ángulos:
class Caja<T>. Dentro de la clase,Tse usa como cualquier otro tipo. Al usarla se concreta:new Caja<int>(42). - Una función genérica lo declara tras su nombre:
T Primero<T>(List<T> lista). Casi siempre no hace falta escribir el tipo al llamarla: el compilador lo deduce de los argumentos. - Puede haber varios parámetros de tipo:
Par<A, B>,Dictionary<K, V>. Por convención se usan letras mayúsculas sueltas, oTElementosi quieres más claridad. - Los genéricos funcionan también en
record,struct,interfaceydelegate(Func<T, R>es un delegado genérico).
Un ejemplo¶
var cajaNumero = new Caja<int>(42);
var cajaTexto = new Caja<string>("hola");
Console.WriteLine($"{cajaNumero.Valor + 1} y {cajaTexto.Valor.ToUpper()}");
Console.WriteLine(cajaNumero);
Console.WriteLine(cajaTexto);
// el compilador deduce el tipo en las funciones genéricas
Console.WriteLine(Primero(["a", "b", "c"]));
Console.WriteLine(Primero([10, 20]));
var (x, y) = Intercambiar(1, "uno");
Console.WriteLine($"{x} / {y}");
T Primero<T>(List<T> lista) => lista[0];
(B, A) Intercambiar<A, B>(A a, B b) => (b, a);
class Caja<T>(T valor)
{
public T Valor { get; } = valor;
public override string ToString() => $"Caja<{typeof(T).Name}>({Valor})";
}
Salida:
43 y HOLA
Caja<Int32>(42)
Caja<String>(hola)
a
10
uno / 1
typeof(T).Name demuestra que T existe en tiempo de ejecución: una Caja<int> y una Caja<string> son tipos distintos de verdad (se explica más en A2.C). Y Intercambiar devuelve una tupla de tipos (B, A): el compilador sabe que x es un string y y un int.
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
Usar ArrayList o object para guardar «cualquier cosa» |
Usar List<T> y genéricos |
Escribir new T() o comparar con < dentro de una función genérica |
T no se sabe hacer eso: hay que restringirlo (A2.B) |
| Especificar el tipo cuando se puede deducir | Dejar que el compilador lo deduzca, salvo que haya ambigüedad |
| Ponerle nombres largos al parámetro de tipo sin necesidad | T basta; usa TClave y TValor cuando haya varios |
Para practicar¶
Los ejercicios A2.1 y A2.2 usan estas ideas. Para ver la misma idea escrita en los otros lenguajes: Pasar de uno a otro.