Módulo 14: Colecciones y genéricos
Objetivos del módulo
- Dominar
List<T>,Dictionary<K,V>,Queue<T>,Stack<T>,HashSet<T> - Comprender qué son los genéricos y crear clases genéricas
- Elegir la colección adecuada para cada situación
1. ¿Por qué colecciones?
Los arrays tienen un tamaño fijo: no puedes añadir ni eliminar elementos después de crearlos. Las colecciones son estructuras de datos dinámicas y más potentes.
2. List<T>: la lista dinámica
📘 Concepto:
List<T>es como un array que crece automáticamente. Es la colección más usada en C#.
// Crear una lista
List<string> nombres = new List<string>();
// Añadir elementos
nombres.Add("Ana");
nombres.Add("Luis");
nombres.Add("María");
// Insertar en posición
nombres.Insert(1, "Pedro"); // Inserta en índice 1
// Acceder por índice
Console.WriteLine(nombres[0]); // Ana
Console.WriteLine(nombres[^1]); // María (último)
// Tamaño
Console.WriteLine($"Total: {nombres.Count}"); // 4
// Recorrer
foreach (string nombre in nombres)
{
Console.WriteLine(nombre);
}
// Eliminar
nombres.Remove("Pedro"); // Elimina por valor
nombres.RemoveAt(0); // Elimina por índice
nombres.RemoveAll(n => n.StartsWith("M")); // Elimina por condición
// Comprobar
bool existe = nombres.Contains("Luis"); // True
// Buscar
string? encontrado = nombres.Find(n => n.Length > 3); // Primer match
List<string> varios = nombres.FindAll(n => n.Length > 3); // Todos los match
int indice = nombres.IndexOf("Luis"); // Posición
// Ordenar
nombres.Sort(); // Alfabético
nombres.Reverse(); // Invertir
nombres.Sort((a, b) => a.Length.CompareTo(b.Length)); // Por longitud
// Convertir a array
string[] array = nombres.ToArray();
// Inicialización directa
List<int> numeros = new() { 1, 2, 3, 4, 5 };
Métodos de List resumidos
| Método | Descripción |
|---|---|
Add(elem) | Añadir al final |
Insert(i, elem) | Insertar en posición |
Remove(elem) | Eliminar primera ocurrencia |
RemoveAt(i) | Eliminar por índice |
RemoveAll(cond) | Eliminar todos que cumplan condición |
Contains(elem) | ¿Existe? |
Find(cond) | Primer elemento que cumple condición |
FindAll(cond) | Todos los que cumplen |
IndexOf(elem) | Posición del elemento |
Sort() | Ordenar |
Reverse() | Invertir |
Count | Número de elementos |
Clear() | Vaciar la lista |
3. Dictionary<TKey, TValue>: pares clave-valor
📘 Concepto: Un
Dictionaryalmacena pares clave-valor. Cada clave es única y permite acceder al valor asociado de forma muy rápida.
// Crear un diccionario
Dictionary<string, int> edades = new()
{
["Ana"] = 25,
["Luis"] = 30,
["María"] = 22
};
// Acceder por clave
Console.WriteLine(edades["Ana"]); // 25
// Añadir
edades["Pedro"] = 28;
// Modificar
edades["Ana"] = 26;
// Comprobar si existe una clave
if (edades.ContainsKey("Luis"))
{
Console.WriteLine($"Luis tiene {edades["Luis"]} años");
}
// Acceso seguro con TryGetValue
if (edades.TryGetValue("Carmen", out int edad))
{
Console.WriteLine($"Carmen tiene {edad} años");
}
else
{
Console.WriteLine("Carmen no encontrada");
}
// Eliminar
edades.Remove("María");
// Recorrer
foreach (KeyValuePair<string, int> par in edades)
{
Console.WriteLine($"{par.Key}: {par.Value} años");
}
// Forma más limpia con desestructuración
foreach (var (nombre, edadVal) in edades)
{
Console.WriteLine($"{nombre}: {edadVal} años");
}
// Obtener solo claves o valores
ICollection<string> claves = edades.Keys;
ICollection<int> valores = edades.Values;
Console.WriteLine($"Personas: {string.Join(", ", claves)}");
Ejemplo práctico: contador de palabras
string texto = "el gato y el perro y el pez";
string[] palabras = texto.Split(' ');
Dictionary<string, int> contador = new();
foreach (string palabra in palabras)
{
if (contador.ContainsKey(palabra))
contador[palabra]++;
else
contador[palabra] = 1;
}
foreach (var (palabra, cuenta) in contador)
{
Console.WriteLine($"'{palabra}': {cuenta} veces");
}
// 'el': 3 veces
// 'gato': 1 veces
// 'y': 2 veces
// 'perro': 1 veces
// 'pez': 1 veces
4. Queue<T>: cola (FIFO)
📘 Concepto: Una cola funciona como una fila de un supermercado: el primero que entra es el primero que sale (FIFO - First In, First Out).
Queue<string> cola = new();
// Encolar (añadir al final)
cola.Enqueue("Cliente 1");
cola.Enqueue("Cliente 2");
cola.Enqueue("Cliente 3");
Console.WriteLine($"En cola: {cola.Count}"); // 3
// Ver el primero sin sacarlo
Console.WriteLine($"Siguiente: {cola.Peek()}"); // Cliente 1
// Desencolar (sacar el primero)
while (cola.Count > 0)
{
string cliente = cola.Dequeue();
Console.WriteLine($"Atendiendo a: {cliente}");
}
// Atendiendo a: Cliente 1
// Atendiendo a: Cliente 2
// Atendiendo a: Cliente 3
5. Stack<T>: pila (LIFO)
📘 Concepto: Una pila funciona como una pila de platos: el último que pones es el primero que sacas (LIFO - Last In, First Out).
Stack<string> historial = new();
// Apilar (push)
historial.Push("google.com");
historial.Push("github.com");
historial.Push("stackoverflow.com");
// Ver lo de arriba sin sacarlo
Console.WriteLine($"Actual: {historial.Peek()}"); // stackoverflow.com
// Desapilar (pop): retroceder en el historial
Console.WriteLine($"Atrás: {historial.Pop()}"); // stackoverflow.com
Console.WriteLine($"Actual: {historial.Peek()}"); // github.com
Ejemplo: deshacer/rehacer
Stack<string> historial = new();
Stack<string> rehacer = new();
string textoActual = "";
void EscribirTexto(string nuevoTexto)
{
historial.Push(textoActual); // Guardar estado anterior
rehacer.Clear(); // Limpiar rehacer al hacer un cambio
textoActual = nuevoTexto;
}
void Deshacer()
{
if (historial.Count > 0)
{
rehacer.Push(textoActual);
textoActual = historial.Pop();
}
}
void Rehacer()
{
if (rehacer.Count > 0)
{
historial.Push(textoActual);
textoActual = rehacer.Pop();
}
}
EscribirTexto("Hola");
EscribirTexto("Hola mundo");
EscribirTexto("Hola mundo!");
Console.WriteLine(textoActual); // "Hola mundo!"
Deshacer();
Console.WriteLine(textoActual); // "Hola mundo"
Deshacer();
Console.WriteLine(textoActual); // "Hola"
Rehacer();
Console.WriteLine(textoActual); // "Hola mundo"
6. HashSet<T>: conjunto sin duplicados
📘 Concepto: Un
HashSetalmacena elementos únicos (sin duplicados). Comprobar si un elemento existe es muy rápido.
HashSet<string> frutas = new() { "Manzana", "Plátano", "Naranja" };
// Añadir (devuelve false si ya existe)
bool añadido1 = frutas.Add("Fresa"); // true
bool añadido2 = frutas.Add("Manzana"); // false (ya existe)
Console.WriteLine($"Total: {frutas.Count}"); // 4
// Comprobar existencia (muy rápido)
Console.WriteLine(frutas.Contains("Naranja")); // True
// Operaciones de conjuntos
HashSet<string> tropicales = new() { "Plátano", "Mango", "Piña" };
// Unión: todas las frutas (de ambos conjuntos)
HashSet<string> union = new(frutas);
union.UnionWith(tropicales);
Console.WriteLine($"Unión: {string.Join(", ", union)}");
// Intersección: solo las que están en ambos
HashSet<string> comunes = new(frutas);
comunes.IntersectWith(tropicales);
Console.WriteLine($"Comunes: {string.Join(", ", comunes)}"); // Plátano
// Diferencia: las que están en frutas pero no en tropicales
HashSet<string> diferencia = new(frutas);
diferencia.ExceptWith(tropicales);
Console.WriteLine($"Solo en frutas: {string.Join(", ", diferencia)}");
7. ¿Cuándo usar cada colección?
| Necesitas… | Usa |
|---|---|
| Lista ordenada con acceso por índice | List<T> |
| Buscar valor por clave | Dictionary<TKey, TValue> |
| Cola de espera (FIFO) | Queue<T> |
| Historial / deshacer (LIFO) | Stack<T> |
| Elementos únicos sin duplicados | HashSet<T> |
| Lista enlazada | LinkedList<T> |
| Lista que no cambia | ImmutableList<T> |
8. Genéricos: crear clases flexibles
📘 Concepto: Los genéricos permiten crear clases, métodos e interfaces que funcionan con cualquier tipo sin perder la seguridad de tipos.
List<T>es un ejemplo:Tse reemplaza por el tipo que elijas.
Método genérico
// T es un "placeholder" para cualquier tipo
static T ObtenerMayor<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Console.WriteLine(ObtenerMayor(10, 20)); // 20
Console.WriteLine(ObtenerMayor("abc", "xyz")); // xyz
Console.WriteLine(ObtenerMayor(3.14, 2.71)); // 3.14
Clase genérica
class Caja<T>
{
private T _contenido;
public Caja(T contenido)
{
_contenido = contenido;
}
public T ObtenerContenido() => _contenido;
public void CambiarContenido(T nuevo) => _contenido = nuevo;
public override string ToString() => $"Caja con: {_contenido}";
}
var cajaNumeros = new Caja<int>(42);
Console.WriteLine(cajaNumeros.ObtenerContenido()); // 42
var cajaTexto = new Caja<string>("Hola");
Console.WriteLine(cajaTexto); // Caja con: Hola
Restricciones genéricas (where)
// T debe implementar IComparable
class ListaOrdenada<T> where T : IComparable<T>
{
private List<T> _items = new();
public void Agregar(T item)
{
_items.Add(item);
_items.Sort();
}
public void MostrarTodos()
{
foreach (T item in _items)
Console.WriteLine(item);
}
}
var listaNumeros = new ListaOrdenada<int>();
listaNumeros.Agregar(30);
listaNumeros.Agregar(10);
listaNumeros.Agregar(20);
listaNumeros.MostrarTodos(); // 10, 20, 30 (ordenados automáticamente)
| Restricción | Significado |
|---|---|
where T : class | T debe ser tipo referencia |
where T : struct | T debe ser tipo valor |
where T : new() | T debe tener constructor sin parámetros |
where T : IComparable<T> | T debe implementar una interfaz |
where T : ClaseBase | T debe heredar de una clase |
where T : notnull | T no puede ser null |
9. Ejercicios
Ejercicio 1: Agenda de contactos
Crea una agenda usando Dictionary<string, List<string>> donde la clave es el nombre y el valor es una lista de teléfonos (una persona puede tener varios). Permite: añadir contacto/teléfono, buscar, eliminar, listar.
Ejercicio 2: Cola de impresión
Simula una cola de impresión con Queue. Cada documento tiene: nombre, páginas, prioridad. Los de alta prioridad se atienden antes. Muestra el proceso de impresión.
Ejercicio 3: Detector de duplicados
Usa HashSet para: detectar palabras duplicadas en un texto, encontrar números que aparecen en dos arrays, y eliminar duplicados de una lista.
Ejercicio 4: Clase genérica Repository
Crea un Repository<T> genérico con: Add(T item), GetById(int id), GetAll(), Delete(int id), Update(T item). Restricción: T debe implementar una interfaz IIdentificable con propiedad Id. Úsalo con classes Producto y Cliente.
Resumen
| Colección | Característica | Acceso |
|---|---|---|
List<T> | Lista dinámica | Por índice |
Dictionary<K,V> | Pares clave-valor | Por clave |
Queue<T> | Cola FIFO | Enqueue / Dequeue |
Stack<T> | Pila LIFO | Push / Pop |
HashSet<T> | Sin duplicados | Contains (rápido) |