Dictionary<TKey, TValue> speichert Werte unter eindeutigen Schlüsseln und findet einen Wert über seinen Schlüssel in ungefähr konstanter Zeit, egal wie viele Einträge es gibt. Es ist das C#-Gegenstück zu einer Hash Map: ein Telefonbuch von Name zu Nummer, ein Cache von ID zu Datensatz, ein Zähler pro Wort.
Ein Dictionary erzeugen und Werte lesen
Ausgabe:
12
2.50
2
True
False
Beide Initialisiererformen tun dasselbe. Die Form ["key"] = value (C# 6) verwendet den Indexer, ein wiederholter Schlüssel überschreibt also; die Form { key, value } ruft Add auf, ein wiederholter Schlüssel wirft also, wenn die Zeile läuft.
ContainsKey ist ein Hash-Nachschlagen und schnell. ContainsValue muss jeden Eintrag durchsuchen, weil Werte nicht indiziert sind.
Add gegenüber Indexer gegenüber TryAdd
Es gibt drei Wege, einen Eintrag einzufügen, und sie unterscheiden sich nur darin, was passiert, wenn der Schlüssel schon existiert:
Ausgabe:
26
Caught ArgumentException
True
False
31
Dass Add bei einem Duplikat wirft, ist ein Feature: Es bringt Daten ans Licht, die eindeutig sein sollten, es aber nicht waren. Nimm den Indexer, wenn du „einfügen oder aktualisieren“ meinst, und TryAdd (ab .NET Core 2.0), wenn der erste Wert gewinnen soll.
Schlüssel können nicht null sein. Add(null, ...) oder dict[null] wirft ArgumentNullException. Werte können null sein, wenn der Werttyp es erlaubt.
KeyNotFoundException und TryGetValue
Einen nicht vorhandenen Schlüssel mit dem Indexer zu lesen wirft KeyNotFoundException. Das ist der häufigste Dictionary-Fehler, und die Lösung ist fast immer TryGetValue.
Ausgabe:
Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email
TryGetValue macht ein einziges Hash-Nachschlagen und meldet den Erfolg als bool. Das Muster if (dict.ContainsKey(k)) { var v = dict[k]; } funktioniert, schlägt den Schlüssel aber zweimal nach. Fehlt der Schlüssel, bekommt die out-Variable den Standardwert des Typs (null, 0, false).
Ab .NET Core 2.0 gibt es außerdem GetValueOrDefault(key, fallback), das den Ersatzwert zurückgibt, wenn der Schlüssel fehlt: emails.GetValueOrDefault(103, "no email").
Einträge aktualisieren und entfernen
Ausgabe:
2
True
False
1
0
cart["milk"] += 1 wirft KeyNotFoundException, wenn milk noch nicht im Dictionary ist, weil es liest, bevor es schreibt. Remove gibt bei einem fehlenden Schlüssel false zurück, statt zu werfen, eine vorherige Prüfung mit ContainsKey ist also nicht nötig.
Durchlaufen: KeyValuePair, Keys und Values
Ein foreach über ein Dictionary liefert KeyValuePair<TKey, TValue>-Elemente, jedes mit einem Key und einem Value.
Ausgabe:
Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe
Total 255
77
pair.Value ist schreibgeschützt, Werte werden also über den Indexer aktualisiert. Die letzte Schleife durchläuft eine List<string>-Kopie der Schlüssel, was immer sicher ist; scores.Keys direkt zu durchlaufen und dabei bestehende Werte zu überschreiben ist ab .NET Core 3.0 erlaubt, wirft aber unter .NET Framework InvalidOperationException.
Einen neuen Schlüssel in einem foreach über dasselbe Dictionary hinzuzufügen wirft in jeder Version InvalidOperationException. Entfernen während des Durchlaufens wirft unter .NET Framework und ist ab .NET Core 3.0 erlaubt. Code, der überall laufen muss, sammelt die zu entfernenden Schlüssel zuerst und entfernt sie nach der Schleife.
Mit C# 7 und .NET Core 2.0 oder neuer lässt sich KeyValuePair im Schleifenkopf dekonstruieren:
foreach (var (name, score) in scores)
{
Console.WriteLine($"{name}: {score}");
}
Mit einem Dictionary zählen
Vorkommen zu zählen ist der Lehrbuchfall. Lies den aktuellen Zählerstand mit TryGetValue (ein fehlender Schlüssel ergibt 0) und schreibe dann zurück.
Ausgabe:
the 3
cat 1
and 2
dog 1
bird 1
Dieselbe Form gruppiert Elemente: Dictionary<string, List<Order>>, bei dem du die Liste mit TryGetValue holst, eine erzeugst und speicherst, wenn sie fehlt, und dann mit Add hinzufügst. Für einmaliges Zählen und Gruppieren erledigt LINQ das in einem Ausdruck: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). Siehe LINQ.
Schlüssel ohne Groß- und Kleinschreibung mit einem Comparer
String-Schlüssel werden standardmäßig exakt verglichen: "Apple" und "apple" sind zwei Schlüssel. Übergib dem Konstruktor einen IEqualityComparer<string>, um das zu ändern.
Ausgabe:
False
text/html
1
StringComparer.OrdinalIgnoreCase ist die richtige Wahl für Bezeichner wie HTTP-Header, Dateiendungen und Benutzernamen. Vor dem Speichern .ToLower() auf jeden Schlüssel anzuwenden funktioniert auch, wird aber leicht an einer Stelle vergessen.
Bei Schlüsseln deiner eigenen Klasse ruft das Dictionary GetHashCode und Equals des Schlüssels auf. Eine Klasse, die sie nicht überschreibt, vergleicht per Referenz, zwei separate Objekte mit denselben Feldern sind also verschiedene Schlüssel. Wie du dieses Paar schreibst, steht unter HashSet.
Reihenfolge, Sortieren und SortedDictionary
Ein Dictionary gibt kein Versprechen über die Reihenfolge beim Durchlaufen. In der Praxis durchläuft ein Dictionary, dem nur Einträge hinzugefügt wurden, in Einfügereihenfolge, aber nach einem Remove kann ein späteres Add den freien Platz wiederverwenden und früher erscheinen. Code sollte sich nie darauf verlassen.
Wenn du eine Reihenfolge brauchst, sortiere an der Stelle, wo du sie brauchst, oder nimm eine sortierte Collection:
Ausgabe:
Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo
SortedDictionary<TKey, TValue> hält seine Schlüssel jederzeit sortiert (es ist ein balancierter Baum), Nachschlagen und Einfügen kosten also O(log n) statt O(1). Nimm es, wenn du oft in Schlüsselreihenfolge durchläufst; sortiere ein normales Dictionary mit LINQ, wenn du die Reihenfolge nur einmal brauchst. SortedList<TKey, TValue> ist eine dritte Option, die weniger Speicher braucht, aber bei großer Größe langsam beim Einfügen ist.
Kurzübersicht
| Aufgabe | Code |
|---|---|
| Erzeugen | new Dictionary<string, int>() |
| Einfügen oder überschreiben | d[k] = v |
| Einfügen, bei Duplikat werfen | d.Add(k, v) |
| Nur einfügen, wenn neu | d.TryAdd(k, v) |
| Lesen, werfen wenn fehlend | d[k] |
| Sicher lesen | d.TryGetValue(k, out var v) |
| Schlüssel existiert | d.ContainsKey(k) |
| Entfernen | d.Remove(k) (gibt bool zurück) |
| Größe | d.Count |
| Schlüssel, Werte | d.Keys, d.Values |
| Nach Schlüssel sortiert | d.OrderBy(p => p.Key) oder SortedDictionary |
| Schreibweise ignorieren | new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
Häufige Fehler
- Einen fehlenden Schlüssel mit
d[k]lesen. WirftKeyNotFoundException; nimmTryGetValue. Addfür einen Schlüssel aufrufen, der existieren könnte. WirftArgumentException; nimm den Indexer oderTryAdd.- In
foreachüber das Dictionary Schlüssel hinzufügen. WirftInvalidOperationException; sammle Änderungen und wende sie danach an. - Sich auf die Reihenfolge beim Durchlaufen verlassen. Sortiere oder nimm
SortedDictionary. - Felder eines Schlüsselobjekts nach dem Einfügen ändern. Sein Hashcode ändert sich, und der Eintrag lässt sich nicht mehr finden.
ContainsKeyund dann Indexer. Zwei Nachschlagevorgänge;TryGetValuemacht einen.
Häufig gestellte Fragen
Was ist der Unterschied zwischen Dictionary.Add und dem Indexer in C#?
dict.Add(key, value) fügt einen neuen Eintrag ein und wirft ArgumentException, wenn der Schlüssel schon existiert. dict[key] = value fügt den Eintrag ein, wenn der Schlüssel neu ist, überschreibt den Wert, wenn er existiert, und wirft bei einem Duplikat nie. TryAdd(key, value) fügt nur ein, wenn der Schlüssel neu ist, und gibt sonst false zurück.
Wie funktioniert TryGetValue in C#?
dict.TryGetValue(key, out var value) gibt true zurück und setzt value, wenn der Schlüssel existiert, und gibt false zurück und setzt value auf den Standardwert seines Typs, wenn nicht. Es macht ein Nachschlagen, wo ContainsKey gefolgt von dict[key] zwei macht, und wirft nie KeyNotFoundException.
Wie durchlaufe ich in C# ein Dictionary?
foreach (KeyValuePair<string, int> pair in dict) liefert jeden Eintrag mit pair.Key und pair.Value. Um nur Schlüssel oder nur Werte zu durchlaufen, nimm dict.Keys oder dict.Values. Füge in dieser Schleife keine Schlüssel zum Dictionary hinzu: Das wirft InvalidOperationException.
Ist ein C#-Dictionary geordnet?
Es wird keine Reihenfolge garantiert. Ein Dictionary, dem nur Einträge hinzugefügt wurden, durchläuft meist in Einfügereihenfolge, aber nach einem Remove können neue Einträge den freien Platz füllen, und die Reihenfolge ändert sich. Sortiere, wenn du eine Reihenfolge brauchst: dict.OrderBy(p => p.Key), oder nimm SortedDictionary<TKey, TValue>, das immer nach Schlüssel durchläuft.
Wie mache ich Dictionary-Schlüssel unabhängig von Groß- und Kleinschreibung?
Übergib dem Konstruktor einen Comparer: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Dann sind "Apple" und "apple" für Nachschlagen, Add und ContainsKey derselbe Schlüssel. Der Comparer wird beim Erzeugen des Dictionarys festgelegt.
Was ist ein KeyValuePair in C#?
KeyValuePair<TKey, TValue> ist das Struct, das dir ein Dictionary beim Durchlaufen für jeden Eintrag gibt. Es hat schreibgeschützte Properties Key und Value, du kannst einen Eintrag darüber also nicht ändern; schreibe stattdessen dict[pair.Key] = newValue (nach der Schleife oder über eine Kopie der Schlüssel).