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

  1. El problema sin genéricos: duplicación de código o object y boxing
  2. Clases genéricas: Repositorio<T>
  3. Métodos genéricos
  4. Restricciones: where T : ...
  5. Especializando Repositorio<T> para BiblioTech
  6. Genéricos con varios parámetros de tipo (anticipo de Dictionary<TKey, TValue>)

  1. El problema sin genéricos: duplicación de código o object y boxing

Sin 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 explicita

Los 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.

  1. Clases genéricas: 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); // 1

Con 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.

  1. 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); // -1

Fí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.

  1. Restricciones: 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

  1. Especializando Repositorio<T> para BiblioTech

Con 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.

  1. Genéricos con varios parámetros de tipo (anticipo de 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 de Repositorio<T> no se puede escribir elemento.Titulo, porque el compilador no sabe que T tendrá 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> y T 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 nombre T, 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 preferir object.
  • Olvidar que default puede ser sorprendente para tipos por valor: default(int) es 0, default(bool) es false; si un método genérico devuelve default para indicar "no encontrado" y T es int, un resultado de 0 podría confundirse con un valor real encontrado. Con tipos por referencia (como MaterialBibliotecario), default es siempre null, más fácil de distinguir con is 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

  1. Define class Repositorio<T> con una lista interna List<T>, un método Agregar(T elemento) y una propiedad Cantidad. Crea un Repositorio<Libro> y un Repositorio<Socio>, añade dos elementos a cada uno y muestra la cantidad de cada repositorio.

  2. Añade a Repositorio<T> un método T? Buscar(Func<T, bool> criterio) que devuelva el primer elemento que cumpla el criterio, o default si ninguno lo cumple. Búscalo por título en un Repositorio<Libro> y por Id en un Repositorio<Socio>.

  3. Define class RepositorioPrestable<T> where T : IPrestable con un método List<T> Disponibles() que devuelva únicamente los elementos con Disponible == true. Añade dos libros (uno prestado, otro disponible) y comprueba que Disponibles() 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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados