Todos los ejemplos de BiblioTech vistos hasta ahora han trabajado con tipos concretos: un
método que recibe un Libro, una lista pensada solo para MaterialBibliotecario. Pero, ¿qué
ocurre cuando necesitas la misma lógica —guardar elementos, buscarlos, contarlos— para
Socio, para Prestamo, o para cualquier tipo futuro que ni siquiera existe todavía? Escribir
una clase distinta para cada tipo sería repetir el mismo código una y otra vez. Los
genéricos resuelven exactamente este problema: permiten escribir una clase o un método una
única vez, dejando el tipo concreto con el que trabajará como un parámetro más, que se decide
al usarlo. Esta lección construye un Repositorio<T> reutilizable y lo especializa para
materiales y socios de BiblioTech, sentando la base sobre la que se construirá la clase
Biblioteca en la próxima lección.
Contenido
- El problema sin genéricos: duplicación de código o
objecty boxing - Clases genéricas:
Repositorio<T> - Métodos genéricos
- Restricciones:
where T : ... - Especializando
Repositorio<T>para BiblioTech - Genéricos con varios parámetros de tipo (anticipo de
Dictionary<TKey, TValue>)
- El problema sin genéricos: duplicación de código o
object y boxing
object y boxingSin genéricos, hay dos formas habituales de escribir una clase reutilizable para "cualquier tipo", y ninguna es satisfactoria. La primera es duplicar la clase entera cambiando el tipo:
class RepositorioDeLibros
{
private List<Libro> _elementos = new List<Libro>();
public void Agregar(Libro elemento) => _elementos.Add(elemento);
}
class RepositorioDeSocios
{
private List<Socio> _elementos = new List<Socio>();
public void Agregar(Socio elemento) => _elementos.Add(elemento);
}El código es idéntico salvo por el tipo: cualquier corrección o mejora debe repetirse en cada
copia. La segunda alternativa, anterior a los genéricos en versiones muy antiguas de .NET, era
usar object como tipo universal:
class RepositorioDeObjetos
{
private List<object> _elementos = new List<object>();
public void Agregar(object elemento) => _elementos.Add(elemento);
}Esto sí es reutilizable, pero tiene dos problemas serios: se pierde toda comprobación de tipos
en tiempo de compilación (nada impide añadir un Socio a un repositorio pensado para Libro,
y el error solo aparecería en tiempo de ejecución al intentar convertir de vuelta), y, para
tipos por valor como int o DateTime, guardarlos como object obliga a un proceso llamado
boxing: envolver el valor en un objeto de memoria dinámica (y unboxing al recuperarlo),
con un coste de rendimiento medible que se repite en cada operación.
List<object> numeros = new List<object>();
numeros.Add(42); // boxing: el int 42 se envuelve en un object en el heap
int valor = (int)numeros[0]; // unboxing: se extrae de vuelta, con conversion explicitaLos genéricos resuelven ambos problemas a la vez: seguridad de tipos en tiempo de compilación, sin duplicar código, y sin boxing para tipos por valor.
- Clases genéricas:
Repositorio<T>
Repositorio<T>Una clase genérica declara uno o más parámetros de tipo entre < y > junto a su nombre
(por convención, una única letra mayúscula como T, de Type), y los usa como si fueran un
tipo normal dentro de la clase:
class Repositorio<T>
{
private List<T> _elementos = new List<T>();
public void Agregar(T elemento)
{
_elementos.Add(elemento);
}
public int Cantidad => _elementos.Count;
}T no es ningún tipo concreto: es un marcador de posición que el compilador sustituye por el
tipo real en el momento de usar la clase, indicado entre <>:
Repositorio<Libro> repositorioLibros = new Repositorio<Libro>();
repositorioLibros.Agregar(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
// repositorioLibros.Agregar(new Socio(1, "Ana Martinez")); // Error de compilacion: Socio no es Libro
Repositorio<Socio> repositorioSocios = new Repositorio<Socio>();
repositorioSocios.Agregar(new Socio(1, "Ana Martinez"));
Console.WriteLine(repositorioLibros.Cantidad); // 1
Console.WriteLine(repositorioSocios.Cantidad); // 1Con una única definición de Repositorio<T>, el compilador genera (conceptualmente) una
versión especializada para cada tipo con el que se use, con comprobación de tipos completa: el
error de intentar añadir un Socio a Repositorio<Libro> se detecta al compilar, no al
ejecutar.
Un método Buscar completa la clase, aceptando un criterio de búsqueda como parámetro (usando
Func<T, bool>, ya conocido de la lección de Delegados y Eventos):
class Repositorio<T>
{
private List<T> _elementos = new List<T>();
public void Agregar(T elemento)
{
_elementos.Add(elemento);
}
public T? Buscar(Func<T, bool> criterio)
{
foreach (T elemento in _elementos)
{
if (criterio(elemento))
{
return elemento;
}
}
return default; // default(T): null para tipos por referencia, 0/false/etc. para tipos por valor
}
public int Cantidad => _elementos.Count;
}default (equivalente a default(T)) devuelve el valor por defecto de T, sea el que sea:
null si T es un tipo por referencia (como Libro), o el valor por defecto correspondiente
(0, false, una fecha mínima...) si T es un tipo por valor. Esto es algo que solo un
genérico puede escribir sin saber de antemano qué tipo es T.
- Métodos genéricos
Además de clases genéricas completas, un único método (dentro de una clase normal o genérica) puede declarar su propio parámetro de tipo, independiente del de la clase que lo contiene:
class Utilidades
{
public static T PrimeroODefault<T>(List<T> lista, T valorPorDefecto)
{
return lista.Count > 0 ? lista[0] : valorPorDefecto;
}
}List<string> nombres = new List<string> { "Ana", "Luis" };
string primero = Utilidades.PrimeroODefault(nombres, "Sin nombre"); // "Ana"
List<int> vacios = new List<int>();
int primerNumero = Utilidades.PrimeroODefault(vacios, -1); // -1Fíjate en que no hizo falta escribir Utilidades.PrimeroODefault<string>(...) explícitamente:
el compilador infiere el tipo T a partir de los argumentos (nombres es List<string>,
así que T es string). Escribir el tipo explícitamente entre <> sigue siendo válido y a
veces necesario, pero rara vez hace falta.
- Restricciones:
where T : ...
where T : ...Sin ninguna restricción, dentro de una clase o método genérico solo se puede asumir que T es
"cualquier tipo", lo que limita mucho lo que se puede hacer con él (ni siquiera se podría
llamar a elemento.Titulo, porque no todo tipo tiene esa propiedad). Una restricción
(where T : ...) reduce el conjunto de tipos aceptados a cambio de poder asumir más sobre
ellos dentro de la clase:
class RepositorioPrestable<T> where T : IPrestable
{
private List<T> _elementos = new List<T>();
public void Agregar(T elemento)
{
_elementos.Add(elemento);
}
public List<T> Disponibles()
{
List<T> resultado = new List<T>();
foreach (T elemento in _elementos)
{
if (elemento.Disponible) // valido: T esta restringido a IPrestable, que declara Disponible
{
resultado.Add(elemento);
}
}
return resultado;
}
}Gracias a where T : IPrestable (recordando la interfaz de la primera lección de este
módulo), el compilador sabe que cualquier T con el que se use esta clase tendrá, como mínimo,
Disponible, Prestar() y Devolver(), y permite usarlos dentro de la clase genérica sin
ninguna conversión adicional.
| Restricción | Significado |
|---|---|
where T : MaterialBibliotecario |
T debe ser MaterialBibliotecario o una clase heredera |
where T : IPrestable |
T debe implementar la interfaz IPrestable |
where T : class |
T debe ser un tipo por referencia (no un struct) |
where T : struct |
T debe ser un tipo por valor |
where T : new() |
T debe tener un constructor público sin parámetros, permitiendo new T() dentro de la clase genérica |
| Varias a la vez | where T : MaterialBibliotecario, IBuscable — deben cumplirse todas |
- Especializando
Repositorio<T> para BiblioTech
Repositorio<T> para BiblioTechCon Repositorio<T> ya definido, se especializa para los dos tipos centrales del modelo de
BiblioTech:
Repositorio<MaterialBibliotecario> repositorioMateriales = new Repositorio<MaterialBibliotecario>();
repositorioMateriales.Agregar(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
repositorioMateriales.Agregar(new Revista("National Geographic", "Varios autores", 302));
MaterialBibliotecario? encontrado = repositorioMateriales.Buscar(m => m.Titulo == "Rayuela");
if (encontrado is Libro libroEncontrado)
{
Console.WriteLine($"Encontrado: {libroEncontrado.Isbn}");
}
Repositorio<Socio> repositorioSocios = new Repositorio<Socio>();
repositorioSocios.Agregar(new Socio(1, "Ana Martinez"));
repositorioSocios.Agregar(new Socio(2, "Luis Gomez"));
Socio? socioEncontrado = repositorioSocios.Buscar(s => s.Id == 2);
Console.WriteLine(socioEncontrado?.Nombre); // "Luis Gomez"Repositorio<MaterialBibliotecario> y Repositorio<Socio> comparten exactamente el mismo
código de Agregar, Buscar y Cantidad, escrito una única vez. Este mismo Repositorio<T>
podría, en principio, respaldar internamente el catálogo de la clase Biblioteca; en la
próxima lección, sin embargo, Biblioteca se construirá directamente sobre List<T> y
Dictionary<TKey, TValue> (las colecciones estándar de .NET), que es el enfoque más habitual
en código real cuando no hace falta la lógica añadida de un repositorio propio. Ambos enfoques
son válidos; lo importante de este apartado es ver los genéricos aplicados a un caso
completo.
- Genéricos con varios parámetros de tipo (anticipo de
Dictionary<TKey, TValue>)
Dictionary<TKey, TValue>)Una clase o método genérico puede declarar más de un parámetro de tipo a la vez, separados por
comas. El ejemplo más habitual, que usarás en la próxima lección, es Dictionary<TKey, TValue>, con dos parámetros de tipo independientes: el tipo de la clave y el tipo del valor
asociado.
class Par<TClave, TValor>
{
public TClave Clave { get; }
public TValor Valor { get; }
public Par(TClave clave, TValor valor)
{
Clave = clave;
Valor = valor;
}
}
Par<int, Socio> parSocio = new Par<int, Socio>(1, new Socio(1, "Ana Martinez"));
Console.WriteLine($"{parSocio.Clave} -> {parSocio.Valor.Nombre}"); // "1 -> Ana Martinez"Esta idea —dos parámetros de tipo independientes, uno para la clave y otro para el valor— es
exactamente la que usa Dictionary<TKey, TValue>, que estudiarás en profundidad en la próxima
lección para indexar los socios de BiblioTech por su Id.
Errores Comunes y Consejos
- Olvidar
where T : ...y esperar poder usar miembros de un tipo concreto: sin restricción, dentro deRepositorio<T>no se puede escribirelemento.Titulo, porque el compilador no sabe queTtendrá esa propiedad; añade la restricción necesaria (where T : MaterialBibliotecario, o una interfaz) para poder acceder a esos miembros. - Confundir el parámetro de tipo de la clase con el de un método:
class Repositorio<T>yT Metodo<T>(...)dentro de una clase no genérica son cosas distintas; un método genérico dentro de una clase genérica puede incluso reutilizar el nombreT, aunque conviene evitarlo para no confundir la lectura. - Usar
object"por simplicidad" en lugar de un genérico: renuncia a la comprobación de tipos en compilación y, para tipos por valor, introduce boxing innecesario; con genéricos disponibles desde las primeras versiones modernas de C#, rara vez hay una buena razón para preferirobject. - Olvidar que
defaultpuede ser sorprendente para tipos por valor:default(int)es0,default(bool)esfalse; si un método genérico devuelvedefaultpara indicar "no encontrado" yTesint, un resultado de0podría confundirse con un valor real encontrado. Con tipos por referencia (comoMaterialBibliotecario),defaultes siemprenull, más fácil de distinguir conis null. - Consejo: nombra los parámetros de tipo con una letra sola cuando el propósito es genérico
y evidente por contexto (
T,TKey,TValue), y con un nombre más descriptivo (TMaterial,TSocio) solo si aporta claridad real en una clase con varios parámetros de tipo relacionados entre sí.
Ejercicios
-
Define
class Repositorio<T>con una lista internaList<T>, un métodoAgregar(T elemento)y una propiedadCantidad. Crea unRepositorio<Libro>y unRepositorio<Socio>, añade dos elementos a cada uno y muestra la cantidad de cada repositorio. -
Añade a
Repositorio<T>un métodoT? Buscar(Func<T, bool> criterio)que devuelva el primer elemento que cumpla el criterio, odefaultsi ninguno lo cumple. Búscalo por título en unRepositorio<Libro>y porIden unRepositorio<Socio>. -
Define
class RepositorioPrestable<T> where T : IPrestablecon un métodoList<T> Disponibles()que devuelva únicamente los elementos conDisponible == true. Añade dos libros (uno prestado, otro disponible) y comprueba queDisponibles()devuelve solo el que está libre.
Soluciones
class Repositorio<T>
{
private List<T> _elementos = new List<T>();
public void Agregar(T elemento)
{
_elementos.Add(elemento);
}
public int Cantidad => _elementos.Count;
}
Repositorio<Libro> repositorioLibros = new Repositorio<Libro>();
repositorioLibros.Agregar(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
repositorioLibros.Agregar(new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
Repositorio<Socio> repositorioSocios = new Repositorio<Socio>();
repositorioSocios.Agregar(new Socio(1, "Ana Martinez"));
repositorioSocios.Agregar(new Socio(2, "Luis Gomez"));
Console.WriteLine(repositorioLibros.Cantidad); // 2
Console.WriteLine(repositorioSocios.Cantidad); // 2
public T? Buscar(Func<T, bool> criterio)
{
foreach (T elemento in _elementos)
{
if (criterio(elemento))
{
return elemento;
}
}
return default;
}
Libro? libroEncontrado = repositorioLibros.Buscar(l => l.Titulo == "Rayuela");
Console.WriteLine(libroEncontrado?.Isbn); // "978-84-376-0495-4"
Socio? socioEncontrado = repositorioSocios.Buscar(s => s.Id == 2);
Console.WriteLine(socioEncontrado?.Nombre); // "Luis Gomez"
class RepositorioPrestable<T> where T : IPrestable
{
private List<T> _elementos = new List<T>();
public void Agregar(T elemento) => _elementos.Add(elemento);
public List<T> Disponibles()
{
List<T> resultado = new List<T>();
foreach (T elemento in _elementos)
{
if (elemento.Disponible)
{
resultado.Add(elemento);
}
}
return resultado;
}
}
RepositorioPrestable<Libro> repositorio = new RepositorioPrestable<Libro>();
Libro disponible = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Libro prestado = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
prestado.Prestar();
repositorio.Agregar(disponible);
repositorio.Agregar(prestado);
List<Libro> disponibles = repositorio.Disponibles();
Console.WriteLine(disponibles.Count); // 1
Console.WriteLine(disponibles[0].Titulo); // "Rayuela"
Conclusión
En esta lección has aprendido a escribir clases y métodos genéricos, a restringir un parámetro
de tipo con where T : ... para poder usar miembros concretos dentro de la clase genérica, y a
evitar tanto la duplicación de código como el boxing de object. Repositorio<T> demuestra
que una única implementación puede servir, de forma segura, tanto para MaterialBibliotecario
como para Socio.
Los genéricos son, precisamente, la base sobre la que están construidas las colecciones de
.NET que usarás a partir de ahora sin pensarlo dos veces: List<T>, que ya conoces desde el
Módulo 1, y Dictionary<TKey, TValue>, con dos parámetros de tipo como el Par<TClave, TValor> del último apartado. La próxima lección, Colecciones, retoma ambas para construir por
fin la clase Biblioteca completa: un catálogo central de materiales y socios, gestionado en
memoria, listo para consultarse con LINQ en la lección siguiente.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
