CSharp

Tutorial CSharp: Diccionarios

Aprende a usar diccionarios en C# para añadir, buscar, eliminar y recorrer datos con eficiencia y seguridad en tus aplicaciones.

Aprende CSharp y certifícate

Dictionary<TKey,TValue>

Los diccionarios son estructuras de datos fundamentales en C# que permiten almacenar información en forma de pares clave-valor. A diferencia de los arrays o listas que utilizan índices numéricos para acceder a los elementos, los diccionarios utilizan claves que pueden ser de casi cualquier tipo para acceder a sus valores correspondientes.

En C#, la clase Dictionary<TKey,TValue> implementa esta estructura de datos, donde:

  • TKey: representa el tipo de datos de la clave
  • TValue: representa el tipo de datos del valor asociado

La principal ventaja de los diccionarios es la búsqueda eficiente. Mientras que buscar un elemento en una lista puede requerir recorrerla completamente (operación O(n)), los diccionarios permiten acceder directamente a un valor conociendo su clave (operación O(1) en promedio).

Creación de un diccionario

Para crear un diccionario en C#, necesitamos especificar los tipos de datos tanto para las claves como para los valores:

// Diccionario con claves de tipo string y valores de tipo int
Dictionary<string, int> edades = new Dictionary<string, int>();

// Diccionario con inicialización de valores
Dictionary<string, string> capitales = new Dictionary<string, string>()
{
    { "España", "Madrid" },
    { "Francia", "París" },
    { "Italia", "Roma" }
};

// Sintaxis alternativa de inicialización (C# 6.0+)
Dictionary<string, string> paises = new Dictionary<string, string>()
{
    ["ES"] = "España",
    ["FR"] = "Francia",
    ["IT"] = "Italia"
};

Características principales

Los diccionarios en C# tienen varias características importantes:

  • Claves únicas: Cada clave solo puede aparecer una vez en el diccionario
  • Acceso rápido: El tiempo de búsqueda es constante en promedio
  • Claves tipadas: Tanto las claves como los valores deben ser del tipo declarado
  • Colección dinámica: Puede crecer o reducirse según sea necesario

Restricciones de las claves

No cualquier tipo puede usarse como clave en un diccionario. Para ser una clave válida, un tipo debe:

  • Implementar correctamente el método GetHashCode() para distribuir las claves uniformemente
  • Implementar correctamente el método Equals() para comparar claves
  • Ser inmutable (no cambiar después de ser añadido al diccionario)

Los tipos comunes que cumplen estos requisitos incluyen:

// Tipos primitivos
Dictionary<int, string> empleadosPorId = new Dictionary<int, string>();
Dictionary<string, double> preciosPorProducto = new Dictionary<string, double>();

// Tipos de valor personalizados (structs)
Dictionary<DateTime, string> eventosPorFecha = new Dictionary<DateTime, string>();

// Tipos de referencia que implementan correctamente Equals y GetHashCode
Dictionary<Guid, Usuario> usuariosPorGuid = new Dictionary<Guid, Usuario>();

Propiedades útiles

Los diccionarios proporcionan varias propiedades que facilitan su uso:

Dictionary<string, int> inventario = new Dictionary<string, int>()
{
    { "Manzanas", 25 },
    { "Naranjas", 32 },
    { "Plátanos", 15 }
};

// Obtener el número de elementos
int cantidad = inventario.Count; // 3

// Obtener colecciones de claves y valores
ICollection<string> productos = inventario.Keys;
ICollection<int> cantidades = inventario.Values;

Casos de uso comunes

Los diccionarios son especialmente útiles en situaciones como:

  • Mapeo de identificadores a objetos: Cuando necesitas acceder rápidamente a objetos por un ID único
Dictionary<int, Cliente> clientesPorId = new Dictionary<int, Cliente>();
clientesPorId.Add(1001, new Cliente { Nombre = "Ana López", Email = "ana@ejemplo.com" });

// Acceso rápido sin necesidad de búsqueda lineal
Cliente cliente = clientesPorId[1001]; // O(1) en promedio
  • Conteo de frecuencias: Para contar ocurrencias de elementos
string texto = "hola mundo hola";
Dictionary<string, int> frecuenciaPalabras = new Dictionary<string, int>();

foreach (string palabra in texto.Split(' '))
{
    if (frecuenciaPalabras.ContainsKey(palabra))
        frecuenciaPalabras[palabra]++;
    else
        frecuenciaPalabras[palabra] = 1;
}

// Resultado: { "hola": 2, "mundo": 1 }
  • Caché de resultados: Para almacenar resultados de operaciones costosas
Dictionary<string, decimal> resultadosCache = new Dictionary<string, decimal>();

decimal CalcularResultado(string entrada)
{
    // Si ya calculamos este valor antes, devolvemos el resultado almacenado
    if (resultadosCache.ContainsKey(entrada))
        return resultadosCache[entrada];
    
    // Simulamos un cálculo costoso
    decimal resultado = decimal.Parse(entrada) * 1.21m;
    
    // Guardamos el resultado para futuras consultas
    resultadosCache[entrada] = resultado;
    
    return resultado;
}

Rendimiento

El rendimiento de los diccionarios es una de sus principales ventajas:

Operación Complejidad promedio
Acceso O(1)
Inserción O(1)
Eliminación O(1)
Búsqueda O(1)

Esto contrasta con las listas, donde la búsqueda tiene una complejidad de O(n), lo que significa que el tiempo de búsqueda aumenta linealmente con el tamaño de la colección.

Consideraciones importantes

Al trabajar con diccionarios, es importante tener en cuenta:

  • Las claves deben ser únicas. Intentar agregar una clave duplicada generará una excepción.
  • Acceder a una clave inexistente con el operador [] lanzará una KeyNotFoundException. Es más seguro usar el método TryGetValue().
  • El orden de los elementos no está garantizado. Si necesitas un orden específico, considera usar SortedDictionary<TKey,TValue> o OrderedDictionary.
  • Los diccionarios no son thread-safe por defecto. Para escenarios concurrentes, considera ConcurrentDictionary<TKey,TValue>.

Los diccionarios son una herramienta fundamental en el arsenal de cualquier programador de C#, permitiendo implementar soluciones eficientes para una amplia variedad de problemas de programación.

Añadir, buscar y eliminar

Trabajar con diccionarios en C# implica dominar las operaciones básicas de manipulación de datos: añadir elementos, buscar información y eliminar entradas. Estas operaciones son fundamentales para aprovechar la eficiencia que ofrecen los diccionarios en aplicaciones reales.

Añadir elementos a un diccionario

Existen varias formas de añadir elementos a un diccionario en C#:

  • Método Add: La forma más explícita de agregar un nuevo par clave-valor.
Dictionary<string, int> puntuaciones = new Dictionary<string, int>();

// Añadir elementos con el método Add
puntuaciones.Add("Juan", 85);
puntuaciones.Add("María", 92);
  • Operador de indexación: Permite tanto añadir como actualizar valores.
// Añadir elementos con el operador de indexación
puntuaciones["Pedro"] = 78;

Es importante tener en cuenta que el método Add lanzará una excepción si intentamos añadir una clave que ya existe, mientras que el operador de indexación simplemente sobrescribirá el valor existente:

// Esto lanzará una System.ArgumentException
// puntuaciones.Add("Juan", 90);

// Esto actualizará el valor sin error
puntuaciones["Juan"] = 90;
  • TryAdd: Disponible desde .NET Core 2.0, permite añadir un elemento solo si la clave no existe.
// Devuelve true si se añadió, false si la clave ya existía
bool fueAgregado = puntuaciones.TryAdd("Ana", 88);

Buscar elementos en un diccionario

La búsqueda es donde los diccionarios realmente destacan por su eficiencia. Hay varias formas de buscar información:

  • Operador de indexación: Acceso directo por clave.
// Acceder a un valor conociendo su clave
int puntuacionJuan = puntuaciones["Juan"]; // 90

Sin embargo, este método lanzará una KeyNotFoundException si la clave no existe, lo que puede causar errores en tiempo de ejecución.

  • Método TryGetValue: La forma más segura de buscar valores.
// Forma segura de obtener un valor
if (puntuaciones.TryGetValue("Luis", out int puntuacionLuis))
{
    Console.WriteLine($"Puntuación de Luis: {puntuacionLuis}");
}
else
{
    Console.WriteLine("Luis no tiene puntuación registrada");
}
  • Método ContainsKey: Verifica si una clave existe antes de intentar acceder a ella.
// Verificar si una clave existe
if (puntuaciones.ContainsKey("María"))
{
    int puntuacion = puntuaciones["María"];
    Console.WriteLine($"Puntuación de María: {puntuacion}");
}
  • Método ContainsValue: Comprueba si un valor específico existe en el diccionario.
// Verificar si existe un valor específico
bool hayAlguienConPuntuacionPerfecta = puntuaciones.ContainsValue(100);

Es importante notar que ContainsValue es menos eficiente que ContainsKey, ya que requiere revisar todos los valores del diccionario (operación O(n)).

Eliminar elementos de un diccionario

Para eliminar elementos de un diccionario, disponemos de varias opciones:

  • Método Remove: Elimina un elemento por su clave.
// Eliminar un elemento por su clave
bool fueEliminado = puntuaciones.Remove("Pedro");

El método Remove devuelve true si el elemento fue eliminado correctamente, o false si la clave no existía.

  • Método Remove con valor de salida: Desde .NET Core 2.0, podemos obtener el valor eliminado.
// Eliminar y obtener el valor eliminado
if (puntuaciones.Remove("María", out int puntuacionEliminada))
{
    Console.WriteLine($"Se eliminó la puntuación de María: {puntuacionEliminada}");
}
  • Método Clear: Elimina todos los elementos del diccionario.
// Eliminar todos los elementos
puntuaciones.Clear();

Patrones comunes de uso

Veamos algunos patrones comunes al trabajar con las operaciones básicas de diccionarios:

  • Actualización condicional: Incrementar un contador solo si la clave existe.
Dictionary<string, int> contadorVisitas = new Dictionary<string, int>();

void RegistrarVisita(string pagina)
{
    if (contadorVisitas.ContainsKey(pagina))
        contadorVisitas[pagina]++;
    else
        contadorVisitas[pagina] = 1;
}
  • Patrón de acceso seguro: Obtener un valor con un valor predeterminado si la clave no existe.
int ObtenerPuntuacion(string nombre)
{
    return puntuaciones.TryGetValue(nombre, out int puntuacion) 
        ? puntuacion 
        : 0; // Valor predeterminado
}
  • Añadir o actualizar (upsert): Patrón común para mantener contadores o acumuladores.
Dictionary<string, int> inventario = new Dictionary<string, int>();

void AgregarProducto(string producto, int cantidad)
{
    if (inventario.TryGetValue(producto, out int cantidadActual))
        inventario[producto] = cantidadActual + cantidad;
    else
        inventario[producto] = cantidad;
}

Este patrón se puede simplificar en versiones más recientes de C# usando el operador de coalescencia nula:

void AgregarProducto(string producto, int cantidad)
{
    inventario[producto] = (inventario.TryGetValue(producto, out int cantidadActual) 
        ? cantidadActual 
        : 0) + cantidad;
}

Manejo de excepciones

Al trabajar con diccionarios, es importante manejar adecuadamente las posibles excepciones:

Dictionary<int, string> empleados = new Dictionary<int, string>();
empleados.Add(101, "Ana Martínez");

try
{
    // Podría lanzar KeyNotFoundException
    string nombre = empleados[102];
}
catch (KeyNotFoundException)
{
    Console.WriteLine("No se encontró el empleado con ID 102");
}

try
{
    // Podría lanzar ArgumentException
    empleados.Add(101, "Ana López"); // Clave duplicada
}
catch (ArgumentException)
{
    Console.WriteLine("Ya existe un empleado con ID 101");
}

Sin embargo, es mejor evitar estas excepciones usando los métodos seguros como TryGetValue y TryAdd cuando sea posible.

Rendimiento y consideraciones prácticas

  • Capacidad inicial: Si conocemos aproximadamente cuántos elementos tendrá nuestro diccionario, podemos mejorar el rendimiento especificando una capacidad inicial.
// Crear un diccionario con capacidad inicial para 1000 elementos
Dictionary<string, Cliente> clientes = new Dictionary<string, Cliente>(1000);
  • Eliminar durante la iteración: No podemos eliminar elementos mientras iteramos un diccionario. Una solución es recopilar las claves a eliminar primero:
Dictionary<string, int> datos = new Dictionary<string, int>
{
    { "A", 1 }, { "B", 2 }, { "C", 3 }, { "D", 4 }
};

// Recopilar claves a eliminar
List<string> clavesAEliminar = new List<string>();
foreach (var par in datos)
{
    if (par.Value % 2 == 0) // Eliminar valores pares
        clavesAEliminar.Add(par.Key);
}

// Eliminar después de la iteración
foreach (string clave in clavesAEliminar)
{
    datos.Remove(clave);
}
  • Verificación de existencia y acceso: Evita verificar la existencia y luego acceder, ya que implica dos búsquedas. Usa TryGetValue en su lugar:
// Evitar esto (dos búsquedas)
if (diccionario.ContainsKey(clave))
{
    var valor = diccionario[clave];
    // Usar valor...
}

// Preferir esto (una sola búsqueda)
if (diccionario.TryGetValue(clave, out var valor))
{
    // Usar valor...
}

Dominar estas operaciones básicas de añadir, buscar y eliminar elementos te permitirá aprovechar al máximo la eficiencia y flexibilidad que ofrecen los diccionarios en C#, facilitando la implementación de soluciones elegantes para una amplia variedad de problemas de programación.

Recorrido de diccionarios

Los diccionarios en C# no solo son útiles para almacenar y recuperar datos mediante claves, sino que también ofrecen diversas formas de recorrer su contenido. Esto es fundamental cuando necesitamos procesar todos los elementos o buscar información basada en criterios más complejos que una simple clave.

Iteración básica con foreach

La forma más común de recorrer un diccionario es mediante un bucle foreach, que nos permite acceder a cada par clave-valor:

Dictionary<string, int> inventario = new Dictionary<string, int>()
{
    { "Manzanas", 150 },
    { "Naranjas", 75 },
    { "Plátanos", 120 }
};

// Recorrer todos los pares clave-valor
foreach (KeyValuePair<string, int> item in inventario)
{
    Console.WriteLine($"Producto: {item.Key}, Cantidad: {item.Value}");
}

En este ejemplo, cada iteración nos proporciona un objeto KeyValuePair<TKey, TValue> que contiene tanto la clave como el valor de cada elemento.

Uso de var para simplificar la sintaxis

Podemos simplificar el código anterior utilizando la inferencia de tipos con var:

// Sintaxis más concisa con var
foreach (var item in inventario)
{
    Console.WriteLine($"Producto: {item.Key}, Cantidad: {item.Value}");
}

Deconstrucción de pares clave-valor (C# 7.0+)

En versiones más recientes de C#, podemos utilizar la deconstrucción para extraer directamente la clave y el valor:

// Deconstrucción de KeyValuePair
foreach (var (producto, cantidad) in inventario)
{
    Console.WriteLine($"Producto: {producto}, Cantidad: {cantidad}");
}

Esta sintaxis hace que el código sea más legible y directo, especialmente cuando trabajamos con nombres de variables significativos.

Recorrido solo de claves

Si solo necesitamos trabajar con las claves del diccionario, podemos iterar sobre la colección Keys:

// Recorrer solo las claves
foreach (string producto in inventario.Keys)
{
    Console.WriteLine($"Producto en inventario: {producto}");
}

Este enfoque es útil cuando solo necesitamos conocer qué elementos están en el diccionario, sin importar sus valores asociados.

Recorrido solo de valores

De manera similar, podemos recorrer únicamente los valores utilizando la colección Values:

// Recorrer solo los valores
foreach (int cantidad in inventario.Values)
{
    Console.WriteLine($"Cantidad en stock: {cantidad}");
}

// Calcular el total de productos
int totalProductos = 0;
foreach (int cantidad in inventario.Values)
{
    totalProductos += cantidad;
}
Console.WriteLine($"Total de productos: {totalProductos}");

Este método es especialmente útil para realizar cálculos o estadísticas sobre los valores almacenados.

Conversión a otras colecciones

A veces, puede ser útil convertir un diccionario a otra estructura de datos para facilitar su procesamiento:

// Convertir a lista de pares clave-valor
List<KeyValuePair<string, int>> listaInventario = inventario.ToList();

// Convertir a arrays separados
string[] productos = inventario.Keys.ToArray();
int[] cantidades = inventario.Values.ToArray();

Estas conversiones son útiles cuando necesitamos pasar los datos a métodos que esperan otros tipos de colecciones o cuando queremos aplicar operaciones específicas de listas o arrays.

Ordenamiento durante el recorrido

Los diccionarios no mantienen un orden específico, pero podemos ordenar los elementos durante el recorrido:

// Recorrer ordenado por clave (orden alfabético de productos)
foreach (var item in inventario.OrderBy(x => x.Key))
{
    Console.WriteLine($"Producto: {item.Key}, Cantidad: {item.Value}");
}

// Recorrer ordenado por valor (de menor a mayor cantidad)
foreach (var item in inventario.OrderBy(x => x.Value))
{
    Console.WriteLine($"Producto: {item.Key}, Cantidad: {item.Value}");
}

// Recorrer ordenado por valor (de mayor a menor cantidad)
foreach (var item in inventario.OrderByDescending(x => x.Value))
{
    Console.WriteLine($"Producto: {item.Key}, Cantidad: {item.Value}");
}

Estos ejemplos utilizan LINQ para ordenar los elementos durante la iteración, sin modificar el diccionario original.

Filtrado durante el recorrido

También podemos usar LINQ para filtrar elementos mientras recorremos el diccionario:

// Filtrar productos con más de 100 unidades
var productosAbundantes = inventario.Where(x => x.Value > 100);
foreach (var item in productosAbundantes)
{
    Console.WriteLine($"Producto abundante: {item.Key} ({item.Value} unidades)");
}

Esta técnica es muy útil para encontrar elementos que cumplan ciertos criterios sin tener que escribir condiciones dentro del bucle foreach.

Recorrido con índice

A diferencia de las listas, los diccionarios no tienen un índice numérico natural. Sin embargo, si necesitamos un índice durante la iteración, podemos crearlo:

// Recorrer con un índice manual
int indice = 0;
foreach (var item in inventario)
{
    Console.WriteLine($"Ítem #{indice}: {item.Key} = {item.Value}");
    indice++;
}

// Alternativa con Select y LINQ
var itemsConIndice = inventario.Select((item, index) => new { Index = index, Key = item.Key, Value = item.Value });
foreach (var item in itemsConIndice)
{
    Console.WriteLine($"Ítem #{item.Index}: {item.Key} = {item.Value}");
}

Recorrido seguro durante modificaciones

Es importante recordar que no podemos modificar un diccionario mientras lo estamos recorriendo con foreach. Si necesitamos hacer esto, debemos tomar un enfoque diferente:

// Crear una copia de las claves para iterar de forma segura
foreach (string producto in new List<string>(inventario.Keys))
{
    if (inventario[producto] == 0)
    {
        inventario.Remove(producto);
        Console.WriteLine($"Producto agotado eliminado: {producto}");
    }
}

En este ejemplo, creamos una copia de las claves para iterar sobre ella, lo que nos permite modificar el diccionario original de forma segura.

Ejemplo práctico: Análisis de texto

Veamos un ejemplo práctico de cómo recorrer un diccionario para analizar la frecuencia de palabras en un texto:

string texto = "El perro persigue al gato y el gato corre rápido porque el perro ladra fuerte";
Dictionary<string, int> frecuenciaPalabras = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

// Contar frecuencia de palabras
foreach (string palabra in texto.Split(' '))
{
    if (frecuenciaPalabras.ContainsKey(palabra))
        frecuenciaPalabras[palabra]++;
    else
        frecuenciaPalabras[palabra] = 1;
}

// Mostrar resultados ordenados por frecuencia
Console.WriteLine("Análisis de frecuencia de palabras:");
foreach (var item in frecuenciaPalabras.OrderByDescending(x => x.Value))
{
    Console.WriteLine($"'{item.Key}': aparece {item.Value} {(item.Value == 1 ? "vez" : "veces")}");
}

Este ejemplo muestra cómo podemos combinar la creación de un diccionario, su recorrido y el uso de LINQ para ordenar los resultados, creando una solución elegante para un problema común.

Rendimiento en recorridos

Al recorrer diccionarios grandes, es importante considerar el rendimiento:

  • El recorrido con foreach tiene una complejidad de O(n), donde n es el número de elementos
  • Recorrer solo Keys o Values tiene el mismo rendimiento que recorrer todo el diccionario
  • Las operaciones de ordenamiento con LINQ (OrderBy, etc.) tienen un costo adicional de O(n log n)
  • La conversión a otras colecciones (ToList, ToArray) requiere memoria adicional

Para diccionarios pequeños, estas consideraciones no son críticas, pero pueden ser importantes cuando trabajamos con grandes volúmenes de datos.

El dominio de las diferentes técnicas de recorrido de diccionarios te permitirá procesar datos de manera eficiente y elegante, aprovechando al máximo esta versátil estructura de datos en tus aplicaciones C#.

Aprende CSharp online

Otras lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

Ejercicios de programación de CSharp

Evalúa tus conocimientos de esta lección Diccionarios con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la estructura y características de la clase Dictionary<TKey,TValue> en C#.
  • Aprender a crear, añadir, buscar y eliminar elementos en un diccionario.
  • Conocer las restricciones y requisitos para usar tipos como claves en diccionarios.
  • Dominar las diferentes formas de recorrer un diccionario y procesar sus elementos.
  • Identificar buenas prácticas y consideraciones de rendimiento al trabajar con diccionarios.