Menu

Dictionary en C#: Add, TryGetValue, recorrer y ordenar

Dictionary<TKey, TValue> asocia claves a valores con búsquedas rápidas. Aprende a añadir y actualizar entradas, leer de forma segura con TryGetValue, recorrer las entradas KeyValuePair, contar apariciones, ignorar mayúsculas en las claves y mantener las claves ordenadas con SortedDictionary.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Dictionary<TKey, TValue> guarda valores bajo claves únicas y encuentra un valor por su clave en un tiempo aproximadamente constante, tenga las entradas que tenga. Es el equivalente en C# de un hash map: una agenda de nombre a número, una caché de ID a registro, un recuento por palabra.

Crear un diccionario y leer valores

Salida:

12
2.50
2
True
False

Las dos formas de inicializador hacen lo mismo. La forma ["key"] = value (C# 6) usa el indexador, así que una clave repetida sobrescribe; la forma { key, value } llama a Add, así que una clave repetida lanza una excepción cuando se ejecuta la línea.

ContainsKey es una búsqueda por hash y es rápida. ContainsValue tiene que recorrer todas las entradas, porque los valores no están indexados.

Add frente al indexador frente a TryAdd

Hay tres formas de meter una entrada, y solo se diferencian en lo que ocurre cuando la clave ya existe:

Salida:

26
Caught ArgumentException
True
False
31

Que Add lance una excepción con un duplicado es una ventaja: saca a la luz datos que deberían haber sido únicos y no lo eran. Usa el indexador cuando lo que quieres decir es "insertar o actualizar", y TryAdd (.NET Core 2.0 y posteriores) cuando debe ganar el primer valor.

Las claves no pueden ser null. Add(null, ...) o dict[null] lanza ArgumentNullException. Los valores sí pueden ser null cuando el tipo de valor lo permite.

KeyNotFoundException y TryGetValue

Leer con el indexador una clave que no está lanza KeyNotFoundException. Es el error más habitual con diccionarios, y la solución es casi siempre TryGetValue.

Salida:

Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email

TryGetValue hace una sola búsqueda por hash e indica el éxito con un bool. El patrón if (dict.ContainsKey(k)) { var v = dict[k]; } funciona, pero busca la clave dos veces. Cuando falta la clave, la variable out recibe el valor por defecto del tipo (null, 0, false).

En .NET Core 2.0 y posteriores existe también GetValueOrDefault(key, fallback), que devuelve el valor alternativo cuando falta la clave: emails.GetValueOrDefault(103, "no email").

Actualizar y eliminar entradas

Salida:

2
True
False
1
0

cart["milk"] += 1 lanza KeyNotFoundException si milk aún no está en el diccionario, porque lee antes de escribir. Remove devuelve false en lugar de lanzar una excepción cuando falta la clave, así que no hace falta comprobar antes ContainsKey.

Recorrer: KeyValuePair, Keys y Values

Un foreach sobre un diccionario produce elementos KeyValuePair<TKey, TValue>, cada uno con un Key y un Value.

Salida:

Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe 
Total 255
77

pair.Value es de solo lectura, así que actualizar valores significa escribir a través del indexador. El último bucle recorre una copia List<string> de las claves, lo que siempre es seguro; recorrer scores.Keys directamente mientras se sobrescriben valores existentes está permitido en .NET Core 3.0 y posteriores, pero lanza InvalidOperationException en .NET Framework.

Añadir una clave nueva dentro de un foreach sobre el mismo diccionario lanza InvalidOperationException en todas las versiones. Eliminar durante la enumeración lanza una excepción en .NET Framework y está permitido desde .NET Core 3.0. El código que debe funcionar en todas partes reúne primero las claves que hay que eliminar y las elimina después del bucle.

Con C# 7 y .NET Core 2.0 o posterior, KeyValuePair puede deconstruirse en la cabecera del bucle:

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

Contar con un diccionario

Contar apariciones es el uso de manual. Lee el recuento actual con TryGetValue (una clave que falta da 0) y después vuelve a escribirlo.

Salida:

the 3
cat 1
and 2
dog 1
bird 1

La misma forma sirve para agrupar elementos: Dictionary<string, List<Order>>, donde obtienes la lista con TryGetValue, creas y guardas una si falta, y después haces Add sobre ella. Para contar y agrupar de una vez, LINQ lo hace en una sola expresión: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). Consulta LINQ.

Claves sin distinguir mayúsculas con un comparador

Por defecto, las claves string se comparan de forma exacta: "Apple" y "apple" son dos claves. Pasa un IEqualityComparer<string> al constructor para cambiarlo.

Salida:

False
text/html
1

StringComparer.OrdinalIgnoreCase es la opción correcta para identificadores como cabeceras HTTP, extensiones de archivo y nombres de usuario. Llamar a .ToLower() sobre cada clave antes de guardarla también funciona, pero es fácil olvidarlo en algún sitio.

Con claves de una clase tuya, el diccionario llama a GetHashCode y Equals de la clave. Una clase que no los sobrescribe compara por referencia, así que dos objetos distintos con los mismos campos son claves diferentes. Consulta HashSet para ver cómo escribir esa pareja de métodos.

Orden, ordenación y SortedDictionary

Un Dictionary no promete nada sobre el orden de enumeración. En la práctica, un diccionario al que solo se han añadido entradas se enumera en orden de inserción, pero después de un Remove, un Add posterior puede reutilizar la posición liberada y aparecer antes. El código nunca debería depender de ello.

Cuando necesites un orden, ordena en el momento de usarlo o usa una colección ordenada:

Salida:

Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo

SortedDictionary<TKey, TValue> mantiene sus claves ordenadas en todo momento (es un árbol equilibrado), así que las búsquedas y las inserciones son O(log n) en lugar de O(1). Úsalo cuando enumeres a menudo en orden de clave; ordena un diccionario normal con LINQ cuando solo necesites el orden una vez. SortedList<TKey, TValue> es una tercera opción que usa menos memoria pero es lenta al insertar cuando es grande.

Referencia rápida

TareaCódigo
Crearnew Dictionary<string, int>()
Insertar o sobrescribird[k] = v
Insertar, excepción si está duplicadad.Add(k, v)
Insertar solo si es nuevad.TryAdd(k, v)
Leer, excepción si faltad[k]
Leer de forma segurad.TryGetValue(k, out var v)
La clave existed.ContainsKey(k)
Eliminard.Remove(k) (devuelve bool)
Tamañod.Count
Claves, valoresd.Keys, d.Values
Ordenado por claved.OrderBy(p => p.Key) o SortedDictionary
Ignorar mayúsculasnew Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

Errores comunes

  • Leer una clave que falta con d[k]. Lanza KeyNotFoundException; usa TryGetValue.
  • Llamar a Add con una clave que puede existir. Lanza ArgumentException; usa el indexador o TryAdd.
  • Añadir claves dentro de un foreach sobre el diccionario. Lanza InvalidOperationException; reúne los cambios y aplícalos después.
  • Depender del orden de enumeración. Ordena, o usa SortedDictionary.
  • Cambiar los campos de un objeto clave después de insertarlo. Su código hash cambia y la entrada ya no se encuentra.
  • ContainsKey y después el indexador. Dos búsquedas; TryGetValue hace una.

Preguntas frecuentes

¿Qué diferencia hay entre Dictionary.Add y el indexador en C#?

dict.Add(key, value) inserta una entrada nueva y lanza ArgumentException si la clave ya existe. dict[key] = value inserta la entrada si la clave es nueva y sobrescribe el valor si ya existe, y nunca lanza una excepción por un duplicado. TryAdd(key, value) inserta solo cuando la clave es nueva y, si no, devuelve false.

¿Cómo funciona TryGetValue en C#?

dict.TryGetValue(key, out var value) devuelve true y asigna value cuando la clave existe, y devuelve false y pone en value el valor por defecto de su tipo cuando no existe. Hace una sola búsqueda, mientras que ContainsKey seguido de dict[key] hace dos, y nunca lanza KeyNotFoundException.

¿Cómo recorro un Dictionary en C#?

foreach (KeyValuePair<string, int> pair in dict) te da cada entrada con pair.Key y pair.Value. Para recorrer solo las claves o solo los valores, usa dict.Keys o dict.Values. No añadas claves al diccionario dentro de ese bucle: lanza InvalidOperationException.

¿Está ordenado un Dictionary de C#?

No se garantiza ningún orden. Un diccionario al que solo se han añadido entradas suele enumerarse en orden de inserción, pero después de un Remove las entradas nuevas pueden ocupar la posición liberada, así que el orden cambia. Ordena cuando necesites un orden: dict.OrderBy(p => p.Key), o usa SortedDictionary<TKey, TValue>, que siempre enumera por clave.

¿Cómo hago que las claves de un Dictionary no distingan mayúsculas?

Pasa un comparador al constructor: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Así "Apple" y "apple" son la misma clave para las búsquedas, Add y ContainsKey. El comparador se fija al crear el diccionario.

¿Qué es un KeyValuePair en C#?

KeyValuePair<TKey, TValue> es el struct que te da un diccionario para cada entrada cuando lo enumeras. Tiene las propiedades de solo lectura Key y Value, así que no puedes cambiar una entrada a través de él; escribe dict[pair.Key] = newValue (después del bucle, o sobre una copia de las claves).

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR