Menu

Dictionary w C#: Add, TryGetValue, iteracja i sortowanie słownika

Dictionary<TKey, TValue> mapuje klucze na wartości z szybkim wyszukiwaniem. Dowiedz się, jak dodawać i aktualizować wpisy, bezpiecznie odczytywać je przez TryGetValue, iterować po wpisach KeyValuePair, liczyć wystąpienia, ignorować wielkość liter w kluczach i utrzymywać posortowane klucze przez SortedDictionary.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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

ZadanieKod
Utworzenienew Dictionary<string, int>()
Wstawienie lub nadpisanied[k] = v
Wstawienie, wyjątek przy duplikacied.Add(k, v)
Wstawienie tylko nowego kluczad.TryAdd(k, v)
Odczyt, wyjątek przy braku kluczad[k]
Bezpieczny odczytd.TryGetValue(k, out var v)
Czy klucz istniejed.ContainsKey(k)
Usunięcied.Remove(k) (zwraca bool)
Rozmiard.Count
Klucze, wartościd.Keys, d.Values
Posortowane po kluczud.OrderBy(p => p.Key) lub SortedDictionary
Bez rozróżniania wielkości liternew Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

Częste błędy

  • Odczyt brakującego klucza przez d[k]. Rzuca KeyNotFoundException; użyj TryGetValue.
  • Wywołanie Add dla klucza, który może istnieć. Rzuca ArgumentException; użyj indeksera lub TryAdd.
  • Dodawanie kluczy wewnątrz foreach po słowniku. Rzuca InvalidOperationException; 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; TryGetValue robi 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).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ