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
| Operazione | Codice |
|---|---|
| Creare | new Dictionary<string, int>() |
| Inserire o sovrascrivere | d[k] = v |
| Inserire, eccezione sui duplicati | d.Add(k, v) |
| Inserire solo se nuova | d.TryAdd(k, v) |
| Leggere, eccezione se manca | d[k] |
| Leggere in sicurezza | d.TryGetValue(k, out var v) |
| La chiave esiste | d.ContainsKey(k) |
| Rimuovere | d.Remove(k) (restituisce bool) |
| Dimensione | d.Count |
| Chiavi, valori | d.Keys, d.Values |
| Ordinato per chiave | d.OrderBy(p => p.Key) o SortedDictionary |
| Ignorare maiuscole e minuscole | new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
Errori comuni
- Leggere una chiave mancante con
d[k]. LanciaKeyNotFoundException; usaTryGetValue. - Chiamare
Addper una chiave che potrebbe esistere. LanciaArgumentException; usa l'indicizzatore oTryAdd. - Aggiungere chiavi dentro un
foreachsul dizionario. LanciaInvalidOperationException; 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ù.
ContainsKeyseguito dall'indicizzatore. Due ricerche;TryGetValuene 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).