En la lección de Arrays y Cadenas de Texto (Módulo 1) conociste List<T> de pasada, con la promesa de estudiarla "en el Módulo 4". Ese momento ha llegado, junto con Dictionary<TKey, TValue> y el resto de colecciones que ofrece .NET. Esta lección repasa List<T> en profundidad, presenta Dictionary<TKey, TValue> para búsquedas rápidas por clave, y —lo más importante de esta lección— construye por fin la clase Biblioteca completa: el punto central que reunirá todo el catálogo de materiales y todos los socios de BiblioTech en memoria, listo para consultarse con LINQ en la próxima lección.

Contenido

  1. Recordatorio: array frente a List<T>
  2. List<T> en profundidad
  3. Dictionary<TKey, TValue>: búsquedas por clave
  4. Otras colecciones: Queue<T> y Stack<T>
  5. Las interfaces detrás de las colecciones: IEnumerable<T> e ICollection<T>
  6. Construyendo Biblioteca: Catalogo, Socios, Prestamos
  7. Uso completo de Biblioteca

  1. Recordatorio: array frente a List<T>

La lección de Arrays y Cadenas de Texto dejó planteada la limitación central de los arrays: su tamaño se fija al crearlos y no puede cambiar después. List<T>, en cambio, crece y encoge de forma dinámica según se le añaden o quitan elementos:

Array (Libro[]) List<T> (List<Libro>)
Tamaño Fijo desde la creación Dinámico: crece y encoge con Add/Remove
Añadir un elemento nuevo No es posible directamente; hay que crear un array más grande lista.Add(elemento)
Eliminar un elemento No es posible directamente lista.Remove(elemento)
Acceso por índice array[0] lista[0] (igual de directo)
Genérico Sí, desde su declaración (Libro[]) Sí, List<T> (Módulo 4)
Cuándo usarlo Tamaño conocido y fijo de antemano El número de elementos cambia durante la ejecución (el caso habitual en BiblioTech)

El catálogo de una biblioteca real crece (se compran libros nuevos) y encoge (se dan de baja materiales deteriorados) constantemente: es exactamente el escenario para el que List<T>, ahora que ya conoces los genéricos, es la elección natural frente a un array.

  1. List<T> en profundidad

List<T> (definida en System.Collections.Generic) ofrece un conjunto de operaciones mucho más rico que un array:

List<MaterialBibliotecario> catalogo = new List<MaterialBibliotecario>();

catalogo.Add(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
catalogo.Add(new Revista("National Geographic", "Varios autores", 302));

Console.WriteLine(catalogo.Count); // 2 (no "Length", como en los arrays)

catalogo.Remove(catalogo[0]); // elimina el primer elemento (busca por igualdad de referencia)
Console.WriteLine(catalogo.Count); // 1

catalogo.Insert(0, new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
Console.WriteLine(catalogo[0].Titulo); // "Ficciones"

bool contieneAlguno = catalogo.Contains(catalogo[0]); // True
Miembro Qué hace
Add(elemento) Añade un elemento al final
Remove(elemento) Elimina la primera aparición de ese elemento
RemoveAt(indice) Elimina el elemento en esa posición
Insert(indice, elemento) Inserta en una posición concreta, desplazando el resto
Contains(elemento) ¿La lista contiene ese elemento exacto?
Count Número de elementos actual (equivalente a Length en un array)
lista[indice] Acceso o asignación directa por índice, igual que en un array

List<T> implementa internamente un array que .NET redimensiona automáticamente cuando hace falta más espacio; esto es un detalle de implementación que no necesitas gestionar tú mismo: desde fuera, List<T> simplemente "crece cuando haga falta".

  1. Dictionary<TKey, TValue>: búsquedas por clave

Recorrer una List<Socio> entera con un foreach para encontrar el socio con Id == 3 es válido, pero ineficiente si el catálogo crece mucho: hay que revisar, en el peor caso, todos los elementos uno a uno. Dictionary<TKey, TValue> —el ejemplo de genérico con dos parámetros de tipo que cerró la lección anterior— resuelve esto asociando cada valor a una clave única, con búsquedas casi instantáneas independientemente de cuántos elementos contenga:

Dictionary<int, Socio> sociosPorId = new Dictionary<int, Socio>();

sociosPorId[1] = new Socio(1, "Ana Martinez");
sociosPorId[2] = new Socio(2, "Luis Gomez");

Socio socio = sociosPorId[1]; // acceso directo por clave, sin recorrer nada
Console.WriteLine(socio.Nombre); // "Ana Martinez"

Acceder a una clave que no existe con sociosPorId[99] lanza una excepción (KeyNotFoundException); TryGetValue es la forma segura de comprobarlo sin arriesgarse a esa excepción:

if (sociosPorId.TryGetValue(99, out Socio? socioEncontrado))
{
    Console.WriteLine(socioEncontrado.Nombre);
}
else
{
    Console.WriteLine("No existe ningun socio con ese Id.");
}
Miembro Qué hace
diccionario[clave] = valor Asigna (o sobrescribe si la clave ya existía)
diccionario[clave] Obtiene el valor; lanza excepción si la clave no existe
TryGetValue(clave, out valor) Intenta obtener el valor; devuelve false sin excepción si no existe
ContainsKey(clave) ¿Existe esa clave?
Remove(clave) Elimina la entrada con esa clave
Keys / Values Colecciones de todas las claves o todos los valores

  1. Otras colecciones: Queue<T> y Stack<T>

.NET ofrece otras colecciones genéricas especializadas en un orden concreto de acceso, útiles en escenarios más específicos:

  • Queue<T> (cola, FIFO — first in, first out): el primer elemento en entrar es el primero en salir, con Enqueue() para añadir y Dequeue() para retirar. Encaja, por ejemplo, con una lista de espera de reservas de un libro muy solicitado: el primer socio en apuntarse es el primero en recibir el libro cuando quede libre.
  • Stack<T> (pila, LIFO — last in, first out): el último elemento en entrar es el primero en salir, con Push() para añadir y Pop() para retirar. Útil para escenarios de tipo "deshacer la última acción", donde importa revertir en orden inverso al que se hizo.
Queue<Socio> listaDeEspera = new Queue<Socio>();
listaDeEspera.Enqueue(new Socio(1, "Ana Martinez")); // se apunta primero
listaDeEspera.Enqueue(new Socio(2, "Luis Gomez"));   // se apunta despues

Socio siguienteEnRecibirlo = listaDeEspera.Dequeue(); // "Ana Martinez": la primera en apuntarse

Ambas se mencionan aquí para que las reconozcas si aparecen en código ajeno; el resto de este módulo se centra en List<T> y Dictionary<TKey, TValue>, que son, con diferencia, las colecciones más usadas en el día a día.

  1. Las interfaces detrás de las colecciones: IEnumerable<T> e ICollection<T>

List<T>, Dictionary<TKey, TValue>, Queue<T> y Stack<T> no son tipos aislados: todas implementan un conjunto común de interfaces (recuerda la lección de Interfaces) que definen capacidades compartidas:

Interfaz Qué garantiza
IEnumerable<T> Se puede recorrer con foreach; es el mínimo común de todas las colecciones
ICollection<T> Además de recorrerse, se puede consultar Count, y añadir/quitar elementos
IList<T> Además de lo anterior, se puede acceder por índice (coleccion[i])

Esta jerarquía de interfaces es exactamente la razón por la que foreach funciona igual sobre un array, una List<T>, o las claves de un Dictionary<TKey, TValue>: todas implementan, como mínimo, IEnumerable<T>. Y es también la razón por la que, en la próxima lección, LINQ podrá operar indistintamente sobre cualquiera de ellas: la mayoría de sus operaciones están definidas sobre IEnumerable<T>, no sobre List<T> en particular.

  1. Construyendo Biblioteca: Catalogo, Socios, Prestamos

Con List<T> y Dictionary<TKey, TValue> ya dominados, se completa la clase Biblioteca, que en la lección de Delegados y Eventos apareció solo con su evento. Esta es su versión completa, con las tres colecciones centrales del modelo y un índice por Id para búsquedas rápidas de socios:

class Biblioteca
{
    public List<MaterialBibliotecario> Catalogo { get; } = new List<MaterialBibliotecario>();
    public List<Socio> Socios { get; } = new List<Socio>();
    public List<Prestamo> Prestamos { get; } = new List<Prestamo>();

    private Dictionary<int, Socio> _sociosPorId = new Dictionary<int, Socio>();

    public event Action<Prestamo> PrestamoRegistrado;

    public void AgregarMaterial(MaterialBibliotecario material)
    {
        Catalogo.Add(material);
    }

    public void AgregarSocio(Socio socio)
    {
        Socios.Add(socio);
        _sociosPorId[socio.Id] = socio;
    }

    public Socio? BuscarSocioPorId(int id)
    {
        _sociosPorId.TryGetValue(id, out Socio? socio);
        return socio;
    }

    public void RegistrarPrestamo(Prestamo prestamo)
    {
        Prestamos.Add(prestamo);
        PrestamoRegistrado?.Invoke(prestamo);
    }
}

Algunas decisiones de diseño merecen explicarse:

  • Catalogo, Socios y Prestamos son propiedades de solo lectura ({ get; }, recordando la lección de Encapsulamiento del Módulo 3): la referencia a cada lista no puede reasignarse desde fuera (biblioteca.Catalogo = otraLista; no compila), pero su contenido sí puede modificarse a través de los métodos pensados para ello (AgregarMaterial, AgregarSocio, RegistrarPrestamo).
  • _sociosPorId es private: es un detalle interno de implementación —un índice para acelerar BuscarSocioPorId— que no debería exponerse ni gestionarse directamente desde fuera de la clase; AgregarSocio se encarga de mantenerlo sincronizado con Socios.
  • El evento PrestamoRegistrado, introducido en la lección anterior, se mantiene sin cambios: ahora convive con las colecciones, y RegistrarPrestamo tanto añade el préstamo a Prestamos como avisa a quien esté suscrito.
classDiagram
    class Biblioteca {
        +List~MaterialBibliotecario~ Catalogo
        +List~Socio~ Socios
        +List~Prestamo~ Prestamos
        -Dictionary~int, Socio~ _sociosPorId
        +event PrestamoRegistrado
        +AgregarMaterial(MaterialBibliotecario)
        +AgregarSocio(Socio)
        +BuscarSocioPorId(int) Socio
        +RegistrarPrestamo(Prestamo)
    }
    Biblioteca --> "*" MaterialBibliotecario
    Biblioteca --> "*" Socio
    Biblioteca --> "*" Prestamo

  1. Uso completo de Biblioteca

Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestamoRegistrado += prestamo =>
    Console.WriteLine($"Se ha prestado '{prestamo.Libro.Titulo}' a {prestamo.Socio.Nombre}");

biblioteca.AgregarMaterial(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
biblioteca.AgregarMaterial(new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
biblioteca.AgregarMaterial(new Revista("National Geographic", "Varios autores", 302));

biblioteca.AgregarSocio(new Socio(1, "Ana Martinez"));
biblioteca.AgregarSocio(new Socio(2, "Luis Gomez"));

Console.WriteLine(biblioteca.Catalogo.Count); // 3
Console.WriteLine(biblioteca.Socios.Count);   // 2

Socio? socio1 = biblioteca.BuscarSocioPorId(1);
if (socio1 is not null && biblioteca.Catalogo[0] is Libro libro1)
{
    libro1.Prestar();
    biblioteca.RegistrarPrestamo(new Prestamo(libro1, socio1));
    // "Se ha prestado 'Rayuela' a Ana Martinez"
}

A partir de esta versión, Biblioteca es el punto único de entrada al estado completo de la aplicación en memoria: todo el catálogo, todos los socios, y el historial de préstamos, cada uno accesible como una colección de .NET completamente estándar.

Errores Comunes y Consejos

  • Usar List<T>.Count esperando Length: los arrays usan Length; List<T>, Dictionary<TKey, TValue> y el resto de colecciones genéricas usan Count. Confundirlos es un error de compilación fácil de corregir, pero frecuente al empezar.
  • Acceder a una clave inexistente con diccionario[clave]: lanza KeyNotFoundException; usa TryGetValue (o ContainsKey antes de acceder) cuando no estés seguro de que la clave existe.
  • Olvidar sincronizar una estructura auxiliar como _sociosPorId: si se añadiera un socio directamente a Socios.Add(...) desde fuera de la clase sin pasar por AgregarSocio, el diccionario quedaría desactualizado; por eso Socios es de solo lectura y toda alta pasa, obligatoriamente, por AgregarSocio.
  • Elegir la colección equivocada para el patrón de acceso: si necesitas buscar constantemente por un identificador único, un Dictionary es mucho más eficiente que recorrer una List<T> entera cada vez; si solo necesitas recorrer todo en orden, List<T> basta.
  • Consejo: antes de elegir una colección, pregúntate cómo vas a acceder a sus datos con más frecuencia (¿por posición?, ¿por clave?, ¿siempre el primero en entrar?); la respuesta señala casi siempre la colección correcta.

Ejercicios

  1. Crea una List<MaterialBibliotecario> con al menos tres elementos (mezcla de Libro y Revista). Añade uno nuevo con Add, elimina uno con Remove, y muestra Count antes y después de cada operación.

  2. Crea un Dictionary<int, Socio> con al menos tres socios, indexados por su Id. Usa TryGetValue para buscar un Id que exista y otro que no, mostrando un mensaje distinto en cada caso sin que el programa lance ninguna excepción.

  3. Construye la clase Biblioteca completa de este apartado. Da de alta dos materiales y un socio, suscríbete al evento PrestamoRegistrado con un mensaje de confirmación, presta uno de los materiales y regístralo con RegistrarPrestamo. Comprueba que Prestamos.Count pasa a valer 1.

Soluciones

List<MaterialBibliotecario> materiales = new List<MaterialBibliotecario>
{
    new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"),
    new Revista("National Geographic", "Varios autores", 302),
    new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1")
};

Console.WriteLine(materiales.Count); // 3

materiales.Add(new Revista("Muy Interesante", "Varios autores", 45));
Console.WriteLine(materiales.Count); // 4

materiales.Remove(materiales[0]);
Console.WriteLine(materiales.Count); // 3
Dictionary<int, Socio> sociosPorId = new Dictionary<int, Socio>
{
    [1] = new Socio(1, "Ana Martinez"),
    [2] = new Socio(2, "Luis Gomez"),
    [3] = new Socio(3, "Marta Lopez")
};

if (sociosPorId.TryGetValue(2, out Socio? encontrado))
{
    Console.WriteLine($"Encontrado: {encontrado.Nombre}"); // "Luis Gomez"
}

if (!sociosPorId.TryGetValue(99, out Socio? noEncontrado))
{
    Console.WriteLine("No existe ningun socio con Id 99.");
}
Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestamoRegistrado += p =>
    Console.WriteLine($"Se ha prestado '{p.Libro.Titulo}' a {p.Socio.Nombre}");

biblioteca.AgregarMaterial(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
biblioteca.AgregarMaterial(new Revista("National Geographic", "Varios autores", 302));
biblioteca.AgregarSocio(new Socio(1, "Ana Martinez"));

Socio? socio = biblioteca.BuscarSocioPorId(1);
if (socio is not null && biblioteca.Catalogo[0] is Libro libro)
{
    libro.Prestar();
    biblioteca.RegistrarPrestamo(new Prestamo(libro, socio));
}

Console.WriteLine(biblioteca.Prestamos.Count); // 1

Conclusión

En esta lección has profundizado en List<T>, conocido Dictionary<TKey, TValue> para búsquedas rápidas por clave, visto de pasada Queue<T> y Stack<T>, y entendido que todas estas colecciones comparten un conjunto común de interfaces (IEnumerable<T>, ICollection<T>). Sobre todo, ha quedado completa la clase Biblioteca: Catalogo, Socios y Prestamos como colecciones centrales, un índice por Id para búsquedas rápidas de socios, y el evento PrestamoRegistrado conviviendo con todo lo demás.

Tener el catálogo completo en una List<MaterialBibliotecario> abre la puerta a la herramienta más potente de este módulo para trabajar con colecciones: LINQ. La próxima lección te enseñará a filtrar, ordenar y agrupar biblioteca.Catalogo y biblioteca.Prestamos con una sintaxis expresiva y compacta, dejando definitivamente atrás los bucles foreach manuales para este tipo de consultas.

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