What a hash map is
A hash map stores key-value pairs. You hand it a key and it gives back the value associated with that key, without scanning through everything it holds. Keys are unique: each key maps to exactly one value, while different keys may share a value.
In C#, the hash map is Dictionary<TKey, TValue> from System.Collections.Generic. The two type arguments are the type of the keys and the type of the values. A Dictionary<string, int> maps strings to integers, for example "two" to 2. Looking up, adding, updating, removing and testing a key all take O(1) time on average, which is why a dictionary is the standard tool for counting, caching and "have I seen this before?" problems.
Creating a dictionary
An empty dictionary is created with new and the two types:
Dictionary<string, int> digitValue = new Dictionary<string, int>();
You can also fill it at creation with a collection initializer. Each pair goes in its own braces:
Dictionary<string, int> digitValue = new Dictionary<string, int> {
{"zero", 0},
{"one", 1}
};
An equivalent index-style initializer is new Dictionary<string, int> { ["zero"] = 0, ["one"] = 1 }. Creating an empty dictionary takes O(1) time.
Reading values
The square-bracket indexer reads a value by key:
int zero = digitValue["zero"]; // 0
int three = digitValue["three"]; // throws KeyNotFoundException
A missing key throws KeyNotFoundException, so the indexer is only safe when you know the key is present. There are three safer options:
ContainsKey(key)returnstrueorfalse, so you can test before you read.GetValueOrDefault(key, fallback)returns the stored value, orfallbackwhen the key is missing. With one argument, the fallback is the type's default, which is0forint.TryGetValue(key, out value)returns abooland writes the value into anoutvariable:if (digitValue.TryGetValue("one", out int v)) { ... }.
Each lookup is O(1).
Adding, updating and removing
Assigning through the indexer adds a key that is not there and replaces the value of a key that is:
digitValue["three"] = 3; // adds
digitValue["zero"] = 5; // updates
digitValue.Remove("one"); // removes
The Add(key, value) method is stricter: it throws ArgumentException if the key already exists. TryAdd(key, value) adds only when the key is new and returns whether it did, so an existing entry is left alone. Remove(key) returns true if it removed something and false if the key was not present, and it never throws for a missing key. All three operations are O(1).
Iterating
A foreach over the dictionary visits KeyValuePair objects, with Key and Value properties. You can also loop over just dict.Keys or dict.Values:
foreach (var pair in digitValue) {
Console.WriteLine(pair.Key + " is " + pair.Value);
}
Iteration takes O(n) time. Do not rely on the order. A dictionary comes with no guarantee about it, even though a dictionary that has only ever had items added often lists them in insertion order. In a test run, a dictionary holding apples, bananas and grapes had bananas removed and then kiwi added; iteration gave apples, kiwi, grapes, because kiwi reused the slot bananas had freed.
Adding a new key to the dictionary while you loop over it throws InvalidOperationException. If you need to add entries while looping, loop over a copy such as dict.Keys.ToList() (this needs using System.Linq;).
Common mistakes
- Reading a missing key with the indexer. The result is an exception, not a default value. Use
GetValueOrDefaultorTryGetValuefor counting patterns. - Calling
Addtwice for the same key. It throws. Use the indexer to overwrite, orTryAddto keep the first value. - Wrong method names. The C# members are
ContainsKey,Remove,AddandKeys, all in PascalCase. Names such asput,getorcontainsKeyand the typeMapare not part ofDictionaryand do not compile. - Using
nullas a key. A dictionary does not accept anullkey and throwsArgumentNullException. - Expecting a fixed order when printing the contents, as covered above.
The exercise
The lesson tracks how many fruits of each kind you have and walks through the operations above.
- Create
fruitsas aDictionary<string, int>initialized with("apples", 3),("bananas", 5)and("grapes", 2). - Add four more apples. Read the current count with
GetValueOrDefault("apples", 0), add 4, and assign the sum back through the indexer. - Report a missing fruit. If
ContainsKey("pears")is false, printOut of pears. - Remove the bananas with
Remove("bananas"). - Print every fruit. Loop with
foreach (string fruit in fruits.Keys)and printWe have, the count fromfruits[fruit], and the fruit's name.
Dictionary<string, int> fruits = new Dictionary<string, int> {
{"apples", 3},
{"bananas", 5},
{"grapes", 2}
};
fruits["apples"] = fruits.GetValueOrDefault("apples", 0) + 4;
if (!fruits.ContainsKey("pears")) {
Console.WriteLine("Out of pears");
}
fruits.Remove("bananas");
foreach (string fruit in fruits.Keys) {
Console.WriteLine("We have " + fruits[fruit] + " " + fruit + ".");
}
Running it prints:
Out of pears
We have 7 apples.
We have 2 grapes.
The lesson accepts the two fruit lines in either order, because a dictionary makes no promise about order.
Key takeaways
Dictionary<TKey, TValue>is C#'s hash map: unique keys, one value per key, andO(1)average operations.- The indexer reads, adds and updates, but reading a missing key throws
KeyNotFoundException. ContainsKey,TryGetValueandGetValueOrDefaultare the safe ways to look up a key that might be absent.Removeis safe on missing keys, andAddis not safe on existing ones.- Iterate with
foreachover the pairs orKeys, and never depend on the order.