Dictionary<TKey, TValue> przechowuje wartości pod unikalnymi kluczami i znajduje wartość po kluczu w przybliżeniu w stałym czasie, niezależnie od liczby wpisów. To odpowiednik mapy haszującej w C#: książka telefoniczna od imienia do numeru, pamięć podręczna od identyfikatora do rekordu, licznik dla każdego słowa.
Tworzenie słownika i odczyt wartości
Wynik:
12
2.50
2
True
False
Obie formy inicjalizatora robią to samo. Forma ["key"] = value (C# 6) używa indeksera, więc powtórzony klucz nadpisuje wartość; forma { key, value } wywołuje Add, więc powtórzony klucz rzuca wyjątek w chwili wykonania tej linii.
ContainsKey to wyszukiwanie po haszu i jest szybkie. ContainsValue musi przejrzeć każdy wpis, bo wartości nie są indeksowane.
Add, indekser i TryAdd
Wpis można wstawić na trzy sposoby, a różnią się tylko tym, co się dzieje, gdy klucz już istnieje:
Wynik:
26
Caught ArgumentException
True
False
31
To, że Add rzuca wyjątek przy duplikacie, jest zaletą: ujawnia dane, które powinny być unikalne, a nie były. Używaj indeksera, gdy chodzi ci o "wstaw lub zaktualizuj", a TryAdd (.NET Core 2.0 i nowsze), gdy wygrać ma pierwsza wartość.
Klucze nie mogą być null. Add(null, ...) lub dict[null] rzuca ArgumentNullException. Wartości mogą być null, jeśli pozwala na to ich typ.
KeyNotFoundException i TryGetValue
Odczyt nieistniejącego klucza przez indekser rzuca KeyNotFoundException. To najczęstszy błąd przy słownikach, a rozwiązaniem prawie zawsze jest TryGetValue.
Wynik:
Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email
TryGetValue wykonuje jedno wyszukiwanie po haszu i zgłasza powodzenie jako bool. Wzorzec if (dict.ContainsKey(k)) { var v = dict[k]; } działa, ale szuka klucza dwa razy. Gdy klucza brakuje, zmienna out dostaje wartość domyślną typu (null, 0, false).
W .NET Core 2.0 i nowszych jest też GetValueOrDefault(key, fallback), które zwraca wartość zastępczą, gdy klucza brakuje: emails.GetValueOrDefault(103, "no email").
Aktualizowanie i usuwanie wpisów
Wynik:
2
True
False
1
0
cart["milk"] += 1 rzuca KeyNotFoundException, jeśli milk jeszcze nie ma w słowniku, bo najpierw odczytuje, a potem zapisuje. Remove przy brakującym kluczu zwraca false zamiast rzucać wyjątek, więc nie trzeba wcześniej sprawdzać ContainsKey.
Iteracja: KeyValuePair, Keys i Values
foreach po słowniku daje elementy KeyValuePair<TKey, TValue>, każdy z Key i Value.
Wynik:
Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe
Total 255
77
pair.Value jest tylko do odczytu, więc aktualizacja wartości oznacza zapis przez indekser. Ostatnia pętla iteruje po kopii kluczy typu List<string>, co jest zawsze bezpieczne; bezpośrednia iteracja po scores.Keys z nadpisywaniem istniejących wartości jest dozwolona w .NET Core 3.0 i nowszych, ale w .NET Framework rzuca InvalidOperationException.
Dodanie nowego klucza wewnątrz foreach po tym samym słowniku rzuca InvalidOperationException w każdej wersji. Usuwanie w trakcie wyliczania rzuca wyjątek w .NET Framework, a od .NET Core 3.0 jest dozwolone. Kod, który musi działać wszędzie, najpierw zbiera klucze do usunięcia, a usuwa je dopiero po pętli.
W C# 7 i .NET Core 2.0 lub nowszym KeyValuePair można zdekonstruować w nagłówku pętli:
foreach (var (name, score) in scores)
{
Console.WriteLine($"{name}: {score}");
}
Liczenie za pomocą słownika
Liczenie wystąpień to podręcznikowe zastosowanie. Odczytaj bieżącą liczbę przez TryGetValue (brakujący klucz daje 0), a potem zapisz ją z powrotem.
Wynik:
the 3
cat 1
and 2
dog 1
bird 1
Ten sam układ służy do grupowania: Dictionary<string, List<Order>>, gdzie pobierasz listę przez TryGetValue, tworzysz i zapisujesz nową, jeśli jej brakuje, a potem wykonujesz na niej Add. Przy jednorazowym liczeniu i grupowaniu LINQ robi to w jednym wyrażeniu: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). Zobacz LINQ.
Klucze bez rozróżniania wielkości liter
Klucze typu string domyślnie porównują się dokładnie: "Apple" i "apple" to dwa klucze. Żeby to zmienić, przekaż do konstruktora IEqualityComparer<string>.
Wynik:
False
text/html
1
StringComparer.OrdinalIgnoreCase to właściwy wybór dla identyfikatorów, takich jak nagłówki HTTP, rozszerzenia plików i nazwy użytkowników. Wywoływanie .ToLower() na każdym kluczu przed zapisaniem też działa, ale łatwo o tym zapomnieć w jednym miejscu.
Dla kluczy będących obiektami twojej klasy słownik wywołuje GetHashCode i Equals klucza. Klasa, która ich nie nadpisuje, porównuje się po referencji, więc dwa osobne obiekty z tymi samymi polami to różne klucze. Jak napisać tę parę metod, pokazuje strona o HashSet.
Kolejność, sortowanie i SortedDictionary
Dictionary nie obiecuje żadnej kolejności wyliczania. W praktyce słownik, do którego tylko dodawano wpisy, wylicza je w kolejności dodania, ale po Remove późniejsze Add może użyć zwolnionego miejsca i pojawić się wcześniej. Kod nigdy nie powinien na tym polegać.
Gdy potrzebujesz kolejności, sortuj w miejscu użycia albo użyj posortowanej kolekcji:
Wynik:
Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo
SortedDictionary<TKey, TValue> cały czas utrzymuje posortowane klucze (to zrównoważone drzewo), więc wyszukiwanie i wstawianie mają złożoność O(log n) zamiast O(1). Używaj go, gdy często wyliczasz wpisy w kolejności kluczy; zwykły słownik sortuj przez LINQ, gdy kolejność potrzebna jest tylko raz. SortedList<TKey, TValue> to trzecia opcja, która zużywa mniej pamięci, ale przy dużym rozmiarze wolno się do niej wstawia.
Ściągawka
| Zadanie | Kod |
|---|---|
| Utworzenie | new Dictionary<string, int>() |
| Wstawienie lub nadpisanie | d[k] = v |
| Wstawienie, wyjątek przy duplikacie | d.Add(k, v) |
| Wstawienie tylko nowego klucza | d.TryAdd(k, v) |
| Odczyt, wyjątek przy braku klucza | d[k] |
| Bezpieczny odczyt | d.TryGetValue(k, out var v) |
| Czy klucz istnieje | d.ContainsKey(k) |
| Usunięcie | d.Remove(k) (zwraca bool) |
| Rozmiar | d.Count |
| Klucze, wartości | d.Keys, d.Values |
| Posortowane po kluczu | d.OrderBy(p => p.Key) lub SortedDictionary |
| Bez rozróżniania wielkości liter | new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
Częste błędy
- Odczyt brakującego klucza przez
d[k]. RzucaKeyNotFoundException; użyjTryGetValue. - Wywołanie
Adddla klucza, który może istnieć. RzucaArgumentException; użyj indeksera lubTryAdd. - Dodawanie kluczy wewnątrz
foreachpo słowniku. RzucaInvalidOperationException; zbierz zmiany i zastosuj je później. - Poleganie na kolejności wyliczania. Sortuj albo użyj
SortedDictionary. - Zmiana pól obiektu klucza po jego wstawieniu. Zmienia się jego hash i wpisu nie da się już znaleźć.
ContainsKey, a potem indekser. Dwa wyszukiwania;TryGetValuerobi jedno.
Najczęściej zadawane pytania
Czym różni się Dictionary.Add od indeksera w C#?
dict.Add(key, value) wstawia nowy wpis i rzuca ArgumentException, jeśli klucz już istnieje. dict[key] = value wstawia wpis, jeśli klucz jest nowy, i nadpisuje wartość, jeśli istnieje, więc nigdy nie rzuca wyjątku przy duplikacie. TryAdd(key, value) wstawia wpis tylko wtedy, gdy klucz jest nowy, a w przeciwnym razie zwraca false.
Jak działa TryGetValue w C#?
dict.TryGetValue(key, out var value) zwraca true i ustawia value, gdy klucz istnieje, a gdy nie istnieje, zwraca false i ustawia value na wartość domyślną jego typu. Wykonuje jedno wyszukiwanie, podczas gdy ContainsKey z następującym po nim dict[key] wykonuje dwa, i nigdy nie rzuca KeyNotFoundException.
Jak iterować po Dictionary w C#?
foreach (KeyValuePair<string, int> pair in dict) daje każdy wpis z pair.Key i pair.Value. Żeby przejść pętlą tylko po kluczach albo tylko po wartościach, użyj dict.Keys lub dict.Values. Nie dodawaj kluczy do słownika wewnątrz takiej pętli: rzuca to InvalidOperationException.
Czy Dictionary w C# jest uporządkowany?
Kolejność nie jest gwarantowana. Słownik, do którego tylko dodawano wpisy, zwykle wylicza je w kolejności dodania, ale po Remove nowe wpisy mogą zająć zwolnione miejsce, więc kolejność się zmienia. Gdy potrzebujesz kolejności, sortuj: dict.OrderBy(p => p.Key), albo użyj SortedDictionary<TKey, TValue>, który zawsze wylicza wpisy według klucza.
Jak sprawić, żeby klucze Dictionary nie rozróżniały wielkości liter?
Przekaż obiekt porównujący do konstruktora: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Wtedy "Apple" i "apple" to ten sam klucz przy wyszukiwaniu, Add i ContainsKey. Obiekt porównujący ustala się przy tworzeniu słownika.
Czym jest KeyValuePair w C#?
KeyValuePair<TKey, TValue> to struktura, którą słownik podaje ci dla każdego wpisu przy wyliczaniu. Ma właściwości Key i Value tylko do odczytu, więc nie zmienisz przez nią wpisu; zamiast tego napisz dict[pair.Key] = newValue (po pętli albo iterując po kopii kluczy).