Menu

Dictionary in C#: Add, TryGetValue, iterare e ordinare

Dictionary<TKey, TValue> associa chiavi a valori con una ricerca veloce. Impara ad aggiungere e aggiornare voci, leggere in sicurezza con TryGetValue, scorrere le voci KeyValuePair, contare le occorrenze, ignorare maiuscole e minuscole nelle chiavi e tenere le chiavi ordinate con SortedDictionary.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Dictionary<TKey, TValue> memorizza valori sotto chiavi univoche e trova un valore tramite la sua chiave in un tempo più o meno costante, qualunque sia il numero di voci. È l'equivalente C# di una hash map: una rubrica dal nome al numero, una cache dall'ID al record, un conteggio per parola.

Creare un dizionario e leggere i valori

Output:

12
2.50
2
True
False

Entrambe le forme di inizializzazione fanno la stessa cosa. La forma ["key"] = value (C# 6) usa l'indicizzatore, quindi una chiave ripetuta sovrascrive; la forma { key, value } chiama Add, quindi una chiave ripetuta lancia un'eccezione quando la riga viene eseguita.

ContainsKey è una ricerca hash ed è veloce. ContainsValue deve scorrere ogni voce, perché i valori non sono indicizzati.

Add vs indicizzatore vs TryAdd

Ci sono tre modi per inserire una voce, e differiscono solo in cosa succede quando la chiave esiste già:

Output:

26
Caught ArgumentException
True
False
31

Il fatto che Add lanci un'eccezione su un duplicato è una funzionalità: fa emergere dati che avrebbero dovuto essere univoci e non lo erano. Usa l'indicizzatore quando intendi "inserisci o aggiorna", e TryAdd (.NET Core 2.0 e successivi) quando deve prevalere il primo valore.

Le chiavi non possono essere null. Add(null, ...) o dict[null] lancia ArgumentNullException. I valori possono essere null se il tipo del valore lo consente.

KeyNotFoundException e TryGetValue

Leggere con l'indicizzatore una chiave che non c'è lancia KeyNotFoundException. È l'errore più comune con i dizionari, e la soluzione è quasi sempre TryGetValue.

Output:

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

TryGetValue fa una sola ricerca hash e segnala il successo con un bool. Il pattern if (dict.ContainsKey(k)) { var v = dict[k]; } funziona ma cerca la chiave due volte. Quando la chiave manca, la variabile out riceve il valore predefinito del tipo (null, 0, false).

Su .NET Core 2.0 e successivi esiste anche GetValueOrDefault(key, fallback), che restituisce il valore di ripiego quando la chiave manca: emails.GetValueOrDefault(103, "no email").

Aggiornare e rimuovere voci

Output:

2
True
False
1
0

cart["milk"] += 1 lancia KeyNotFoundException se milk non è ancora nel dizionario, perché legge prima di scrivere. Remove restituisce false invece di lanciare un'eccezione per una chiave mancante, quindi non serve controllare prima ContainsKey.

Iterare: KeyValuePair, Keys e Values

Un foreach su un dizionario produce elementi KeyValuePair<TKey, TValue>, ognuno con una Key e un Value.

Output:

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

pair.Value è di sola lettura, quindi per aggiornare i valori devi scrivere tramite l'indicizzatore. L'ultimo ciclo scorre una copia List<string> delle chiavi, cosa sempre sicura; scorrere direttamente scores.Keys sovrascrivendo valori esistenti è consentito su .NET Core 3.0 e successivi, ma lancia InvalidOperationException su .NET Framework.

Aggiungere una nuova chiave dentro un foreach sullo stesso dizionario lancia InvalidOperationException in ogni versione. Rimuovere durante l'enumerazione lancia un'eccezione su .NET Framework ed è consentito da .NET Core 3.0. Il codice che deve funzionare ovunque raccoglie prima le chiavi da rimuovere, poi le rimuove dopo il ciclo.

Con C# 7 e .NET Core 2.0 o successivi, KeyValuePair si può decostruire nell'intestazione del ciclo:

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

Contare con un dizionario

Contare le occorrenze è l'uso da manuale. Leggi il conteggio attuale con TryGetValue (una chiave mancante dà 0), poi riscrivilo.

Output:

the 3
cat 1
and 2
dog 1
bird 1

La stessa struttura raggruppa gli elementi: Dictionary<string, List<Order>>, dove recuperi la lista con TryGetValue, ne crei e memorizzi una se manca, poi ci fai Add. Per conteggi e raggruppamenti occasionali, LINQ lo fa in un'unica espressione: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). Vedi LINQ.

Chiavi senza distinzione tra maiuscole e minuscole con un comparatore

Per impostazione predefinita le chiavi stringa si confrontano in modo esatto: "Apple" e "apple" sono due chiavi. Passa un IEqualityComparer<string> al costruttore per cambiare questo comportamento.

Output:

False
text/html
1

StringComparer.OrdinalIgnoreCase è la scelta giusta per identificatori come header HTTP, estensioni di file e nomi utente. Anche chiamare .ToLower() su ogni chiave prima di memorizzarla funziona, ma è facile dimenticarlo in un punto.

Per chiavi di una tua classe, il dizionario chiama GetHashCode ed Equals della chiave. Una classe che non ne fa l'override confronta per riferimento, quindi due oggetti separati con gli stessi campi sono chiavi diverse. Vedi HashSet per sapere come scrivere quella coppia di metodi.

Ordine, ordinamento e SortedDictionary

Un Dictionary non promette nulla sull'ordine di enumerazione. In pratica un dizionario a cui sono state solo aggiunte voci le restituisce in ordine di inserimento, ma dopo un Remove un Add successivo può riutilizzare lo spazio liberato e comparire prima. Il codice non dovrebbe mai farci affidamento.

Quando ti serve un ordine, ordina nel punto in cui usi i dati oppure usa una collezione ordinata:

Output:

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

SortedDictionary<TKey, TValue> tiene le chiavi sempre ordinate (è un albero bilanciato), quindi ricerche e inserimenti costano O(log n) invece di O(1). Usalo quando scorri spesso in ordine di chiave; ordina un dizionario normale con LINQ quando l'ordine ti serve una volta sola. SortedList<TKey, TValue> è una terza opzione che usa meno memoria ma diventa lenta negli inserimenti quando è grande.

Riferimento rapido

OperazioneCodice
Crearenew Dictionary<string, int>()
Inserire o sovrascrivered[k] = v
Inserire, eccezione sui duplicatid.Add(k, v)
Inserire solo se nuovad.TryAdd(k, v)
Leggere, eccezione se mancad[k]
Leggere in sicurezzad.TryGetValue(k, out var v)
La chiave esisted.ContainsKey(k)
Rimuovered.Remove(k) (restituisce bool)
Dimensioned.Count
Chiavi, valorid.Keys, d.Values
Ordinato per chiaved.OrderBy(p => p.Key) o SortedDictionary
Ignorare maiuscole e minuscolenew Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

Errori comuni

  • Leggere una chiave mancante con d[k]. Lancia KeyNotFoundException; usa TryGetValue.
  • Chiamare Add per una chiave che potrebbe esistere. Lancia ArgumentException; usa l'indicizzatore o TryAdd.
  • Aggiungere chiavi dentro un foreach sul dizionario. Lancia InvalidOperationException; raccogli le modifiche e applicale dopo.
  • Fare affidamento sull'ordine di enumerazione. Ordina, oppure usa SortedDictionary.
  • Modificare i campi di un oggetto chiave dopo averlo inserito. Il suo hash code cambia e la voce non si trova più.
  • ContainsKey seguito dall'indicizzatore. Due ricerche; TryGetValue ne fa una.

Domande frequenti

Che differenza c'è tra Dictionary.Add e l'indicizzatore in C#?

dict.Add(key, value) inserisce una nuova voce e lancia ArgumentException se la chiave esiste già. dict[key] = value inserisce la voce se la chiave è nuova e sovrascrive il valore se esiste, e non lancia mai eccezioni per un duplicato. TryAdd(key, value) inserisce solo quando la chiave è nuova e altrimenti restituisce false.

Come funziona TryGetValue in C#?

dict.TryGetValue(key, out var value) restituisce true e imposta value quando la chiave esiste, e restituisce false e imposta value al valore predefinito del suo tipo quando non esiste. Fa una sola ricerca, mentre ContainsKey seguito da dict[key] ne fa due, e non lancia mai KeyNotFoundException.

Come scorro un Dictionary in C#?

foreach (KeyValuePair<string, int> pair in dict) ti dà ogni voce con pair.Key e pair.Value. Per scorrere solo le chiavi o solo i valori, usa dict.Keys o dict.Values. Non aggiungere chiavi al dizionario dentro quel ciclo: lancia InvalidOperationException.

Un Dictionary in C# è ordinato?

Nessun ordine è garantito. Un dizionario a cui sono state solo aggiunte voci di solito le restituisce in ordine di inserimento, ma dopo un Remove le nuove voci possono occupare lo spazio liberato, quindi l'ordine cambia. Ordina quando ti serve un ordine: dict.OrderBy(p => p.Key), oppure usa SortedDictionary<TKey, TValue>, che restituisce sempre le voci in ordine di chiave.

Come rendo le chiavi di un Dictionary indifferenti a maiuscole e minuscole?

Passa un comparatore al costruttore: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). A quel punto "Apple" e "apple" sono la stessa chiave per ricerche, Add e ContainsKey. Il comparatore viene fissato quando il dizionario viene creato.

Cos'è un KeyValuePair in C#?

KeyValuePair<TKey, TValue> è la struct che un dizionario ti consegna per ogni voce quando lo scorri. Ha le proprietà di sola lettura Key e Value, quindi non puoi modificare una voce attraverso di essa; scrivi invece dict[pair.Key] = newValue (dopo il ciclo, o su una copia delle chiavi).

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA