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 Dictionary almacena 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 HashSet almacena 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: T se 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)