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
- Recordatorio: array frente a
List<T> List<T>en profundidadDictionary<TKey, TValue>: búsquedas por clave- Otras colecciones:
Queue<T>yStack<T> - Las interfaces detrás de las colecciones:
IEnumerable<T>eICollection<T> - Construyendo
Biblioteca:Catalogo,Socios,Prestamos - Uso completo de
Biblioteca
- Recordatorio: array frente a
List<T>
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.
List<T> en profundidad
List<T> en profundidadList<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".
Dictionary<TKey, TValue>: búsquedas por clave
Dictionary<TKey, TValue>: búsquedas por claveRecorrer 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 |
- Otras colecciones:
Queue<T> y Stack<T>
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, conEnqueue()para añadir yDequeue()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, conPush()para añadir yPop()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 apuntarseAmbas 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.
- Las interfaces detrás de las colecciones:
IEnumerable<T> e ICollection<T>
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.
- Construyendo
Biblioteca: Catalogo, Socios, Prestamos
Biblioteca: Catalogo, Socios, PrestamosCon 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,SociosyPrestamosson 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)._sociosPorIdesprivate: es un detalle interno de implementación —un índice para acelerarBuscarSocioPorId— que no debería exponerse ni gestionarse directamente desde fuera de la clase;AgregarSociose encarga de mantenerlo sincronizado conSocios.- El evento
PrestamoRegistrado, introducido en la lección anterior, se mantiene sin cambios: ahora convive con las colecciones, yRegistrarPrestamotanto añade el préstamo aPrestamoscomo 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
- Uso completo de
Biblioteca
BibliotecaBiblioteca 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>.CountesperandoLength: los arrays usanLength;List<T>,Dictionary<TKey, TValue>y el resto de colecciones genéricas usanCount. Confundirlos es un error de compilación fácil de corregir, pero frecuente al empezar. - Acceder a una clave inexistente con
diccionario[clave]: lanzaKeyNotFoundException; usaTryGetValue(oContainsKeyantes 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 aSocios.Add(...)desde fuera de la clase sin pasar porAgregarSocio, el diccionario quedaría desactualizado; por esoSocioses de solo lectura y toda alta pasa, obligatoriamente, porAgregarSocio. - Elegir la colección equivocada para el patrón de acceso: si necesitas buscar
constantemente por un identificador único, un
Dictionaryes mucho más eficiente que recorrer unaList<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
-
Crea una
List<MaterialBibliotecario>con al menos tres elementos (mezcla deLibroyRevista). Añade uno nuevo conAdd, elimina uno conRemove, y muestraCountantes y después de cada operación. -
Crea un
Dictionary<int, Socio>con al menos tres socios, indexados por suId. UsaTryGetValuepara buscar unIdque exista y otro que no, mostrando un mensaje distinto en cada caso sin que el programa lance ninguna excepción. -
Construye la clase
Bibliotecacompleta de este apartado. Da de alta dos materiales y un socio, suscríbete al eventoPrestamoRegistradocon un mensaje de confirmación, presta uno de los materiales y regístralo conRegistrarPrestamo. Comprueba quePrestamos.Countpasa a valer1.
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#
- 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
