Dictionary<TKey, TValue>
Key-to-value lookup in roughly constant time: the indexer versus TryGetValue, ContainsKey, and why the order is not guaranteed.
Dictionary<string, int> stock = new Dictionary<string, int>();A Dictionary<TKey, TValue> stores pairs. Each pair has a key you look things up by and a value you get back. Finding something in a list means searching from the start. A dictionary goes more or less straight to the pair however many it holds, because it turns the key into a number and uses that number to decide where the pair lives.
Writing and reading
The indexer, stock["apple"] = 4, adds the pair or overwrites the value if that key is already there. Add(key, value) does the same for a new key but throws ArgumentException if the key exists; TryAdd(key, value) returns false instead of throwing.
Reading is where people get caught. stock["fig"] throws KeyNotFoundException when the key is absent. TryGetValue(key, out value) returns true or false and puts the value into the variable you pass, so it asks and answers in one lookup. ContainsKey(key) followed by the indexer does the same work twice.
Walking the pairs
foreach over a dictionary gives you a KeyValuePair<TKey, TValue> each time, with .Key and .Value on it. The Keys and Values properties give one side on its own.
Order is not guaranteed
The documentation states that the order items come back in is undefined. Never rely on insertion order. If you need a predictable order, copy the keys into a list and sort them, or use SortedDictionary<TKey, TValue>. Keys are matched with equality and hashing, so pass StringComparer.OrdinalIgnoreCase to the constructor when "Host" and "host" should mean the same key.
Example
Dictionary<string, int> stock = new Dictionary<string, int>();
stock["apple"] = 4; // the indexer adds the key, or overwrites its value
stock["pear"] = 7;
stock.Add("plum", 2); // Add throws if the key is already present
stock["apple"] = 5; // same key, new value: still three pairs
Console.WriteLine($"count: {stock.Count}");
Console.WriteLine($"apple: {stock["apple"]}");
// TryGetValue asks and answers in one step, with no exception for a miss.
if (stock.TryGetValue("fig", out int figs))
{
Console.WriteLine($"fig: {figs}");
}
else
{
Console.WriteLine("fig: not stocked");
}
Console.WriteLine($"has pear: {stock.ContainsKey("pear")}");
// The order of the pairs is undefined, so sort the keys before printing.
List<string> keys = new List<string>(stock.Keys);
keys.Sort();
foreach (string key in keys)
{
Console.WriteLine($"{key} = {stock[key]}");
}
// Output:
// count: 3
// apple: 5
// fig: not stocked
// has pear: True
// apple = 5
// pear = 7
// plum = 2When to use it
- Looking a user up by their id without scanning a list of every user.
- Counting how many times each error message appeared in a batch of log lines.
- Holding parsed settings so later code can ask for one by name.
More examples
Counting how often each value appears
Counting or grouping by a key is the most common reason to open a Dictionary. The trick worth keeping is that TryGetValue writes the type's default value on a miss, so you do not need a separate ContainsKey branch.
string[] words = { "red", "blue", "red", "green", "red" };
Dictionary<string, int> counts = new Dictionary<string, int>();
foreach (string word in words)
{
// A key that is missing leaves seenSoFar at 0, which is the right
// starting count, so the same two lines handle both cases.
counts.TryGetValue(word, out int seenSoFar);
counts[word] = seenSoFar + 1;
}
List<string> keys = new List<string>(counts.Keys);
keys.Sort();
foreach (string key in keys)
{
Console.WriteLine($"{key}: {counts[key]}");
}
// Output:
// blue: 1
// green: 1
// red: 3Case-insensitive keys for configuration
Settings, HTTP header names and file extensions are all cases where the casing a user typed should not create a second entry. Choosing the comparer at construction is better than lowercasing every key by hand, because it cannot be forgotten at one call site.
// The comparer you pass decides which keys count as the same key.
Dictionary<string, string> settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Host"] = "localhost",
["Port"] = "8080"
};
Console.WriteLine(settings["host"]); // the "Host" entry
Console.WriteLine(settings.ContainsKey("PORT"));
Console.WriteLine(settings.TryAdd("host", "example.com")); // false: already there
Console.WriteLine(settings["host"]); // so it is unchanged
// Output:
// localhost
// True
// False
// localhostRemoving a pair and keeping the value
Useful whenever removing an entry is also the moment you need to act on it, such as closing a session or releasing something the value owns. Removing a key that is not present is not an error; it simply returns false.
Dictionary<int, string> sessions = new Dictionary<int, string>
{
{ 1, "ana" },
{ 2, "bo" }
};
// Remove reports whether the key was there, and this overload also hands
// back the value it removed.
if (sessions.Remove(2, out string? name))
{
Console.WriteLine($"ended session for {name}");
}
Console.WriteLine($"left: {sessions.Count}");
Console.WriteLine($"still has 2: {sessions.ContainsKey(2)}");
// Output:
// ended session for bo
// left: 1
// still has 2: False
Discussion