A Dictionary stores key/value pairs and finds any value almost instantly — by key, never by scanning.
Imagine an online store with 50,000 products. A customer scans a barcode, and your code needs to find that exact product's price. With a List<Product>, the only way to find it is to check each item one by one until you find a match — in the worst case, checking all 50,000. There has to be a faster way to say "give me the thing that goes with this specific key."
In this lesson, you'll learn what a Dictionary<TKey, TValue> is, why it's dramatically faster than searching a list, how it achieves that speed internally with hashing, and where it fits in real applications.
A dictionary is a lot like a real paper dictionary: instead of reading every page to find a word, you jump straight to it because words are organized so you can find them fast. A C# Dictionary works the same way — you give it a key (like a product SKU), and it hands back the matching value (like the product's details), almost instantly, no matter how many entries it holds.
Dictionary<TKey, TValue> is a generic collection that stores data as key/value pairs. Every key in the dictionary must be unique — you can't have two entries with the same key. Looking up a value by its key is extremely fast because the dictionary uses a technique called hashing (explained in Under the Hood) instead of scanning entries one at a time.
List<T> does.Dictionary<string, decimal> prices = new()
{
["SKU-100"] = 19.99m,
["SKU-200"] = 4.75m,
};
decimal price = prices["SKU-100"]; // 19.99 — instant lookup, not a scanSuppose you keep your product catalog as a List<Product>, and you need to find the product with SKU "SKU-4821":
Product? found = null;
foreach (var product in products)
{
if (product.Sku == "SKU-4821")
{
found = product;
break;
}
}With 50,000 products, this might check nearly all 50,000 of them in the worst case before finding — or failing to find — the match. Every additional product makes every future lookup a little slower. This is called a linear search, and it simply doesn't scale for lookup-heavy code.
You need a way to jump directly to the value associated with a specific key, without checking every other entry along the way — regardless of whether the collection holds 10 items or 10 million.
Dictionary<string, Product> catalog = BuildCatalog();
Product found = catalog["SKU-4821"]; // near-instant, regardless of catalog sizeThe dictionary trades a bit of extra memory and setup for dramatically faster lookups — a trade nearly every real application is happy to make once the collection grows past a handful of items.
Dictionary<string, decimal> prices = [];
prices.Add("SKU-100", 19.99m);
prices["SKU-200"] = 4.75m; // indexer syntax also adds a new entry
Add throws if the key already exists; the indexer ([key] = value) both adds new keys and overwrites existing ones.decimal price = prices["SKU-100"]; // 19.99
decimal missing = prices["SKU-999"]; // throws KeyNotFoundException!
if (prices.TryGetValue("SKU-999", out decimal foundPrice))
Console.WriteLine($"Price: {foundPrice}");
else
Console.WriteLine("Not found.");
TryGetValue is the idiomatic way to look up a value that might not exist — one call, no exception, no separate "does it exist" check first.foreach (KeyValuePair<string, decimal> pair in prices)
Console.WriteLine($"{pair.Key}: {pair.Value:C}");
foreach (string sku in prices.Keys)
Console.WriteLine(sku);
Dictionary<string, int> wordCounts = [];
string[] words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
foreach (string word in words)
{
if (wordCounts.TryGetValue(word, out int count))
wordCounts[word] = count + 1;
else
wordCounts[word] = 1;
}
foreach (var pair in wordCounts)
Console.WriteLine($"{pair.Key}: {pair.Value}");
// apple: 3
// banana: 2
// cherry: 1Code → Meaning → Result:
TryGetValue checks and reads in one step — no risk of throwing if the word hasn't been seen yet.An e-commerce product catalog needs to look up a product by SKU on every scan at checkout — potentially hundreds of times per minute during a sale. A dictionary keeps every lookup fast, no matter how large the catalog grows.
public record Product(string Sku, string Name, decimal Price);
public class ProductCatalog
{
private readonly Dictionary<string, Product> _bySku = [];
public void AddProduct(Product product) => _bySku[product.Sku] = product;
public Product? FindBySku(string sku)
{
return _bySku.TryGetValue(sku, out Product? product) ? product : null;
}
public bool RemoveProduct(string sku) => _bySku.Remove(sku);
public int Count => _bySku.Count;
}
var catalog = new ProductCatalog();
catalog.AddProduct(new Product("SKU-100", "Notebook", 4.75m));
catalog.AddProduct(new Product("SKU-200", "Pen", 2.50m));
Product? scanned = catalog.FindBySku("SKU-100");
Console.WriteLine(scanned is not null
? $"Found: {scanned.Name} — {scanned.Price:C}"
: "Product not found.");
// Found: Notebook — $4.75
Product? missing = catalog.FindBySku("SKU-999");
Console.WriteLine(missing is not null ? missing.Name : "Product not found.");
// Product not found.Whether the catalog holds 50 products or 5 million, FindBySku stays fast — that's the entire reason dictionaries exist.
Imagine a mailroom with a wall of numbered cubbyholes, one per apartment. When mail arrives addressed to "Apartment 4B," the mail sorter doesn't check every single cubby in order — they compute exactly which cubby corresponds to 4B and walk straight to it. The apartment number is the key; the mail sitting in that cubby is the value.
A dictionary works the same way: given a key, it computes exactly which internal "cubby" (called a bucket, explained below) that key belongs to, and goes straight there — no scanning every other cubby along the way.
GetHashCode(). Two equal keys always produce the same hash code."SKU-100", the hash code might come out as some seemingly random number, like 1847293651.bucketIndex = hashCode % bucketCount.Equals() until it finds the exact match.You could represent key/value data as List<(string Key, decimal Value)>, but finding an entry would still mean scanning the whole list — you'd lose the entire speed advantage a real dictionary provides. Use an actual Dictionary<TKey, TValue> whenever fast lookup by key matters.
Unlike List<T>, a Dictionary makes no promise about the order you'll get entries back in when you iterate. In practice it's often close to insertion order for small dictionaries that never remove entries, but this is an implementation detail you should never rely on. If order matters, sort explicitly or use a different structure.
[] vs TryGetValuedict[key] throws KeyNotFoundException if the key isn't present. TryGetValue never throws — it simply returns false and gives you a default value. Use the indexer only when you're certain the key exists; use TryGetValue whenever it might not.
Wrong — throws if the key isn't there:
decimal price = prices["SKU-DOES-NOT-EXIST"]; // KeyNotFoundExceptionCorrect:
if (prices.TryGetValue("SKU-DOES-NOT-EXIST", out decimal price))
Console.WriteLine(price);
else
Console.WriteLine("Not found.");Add for a key that might already exist Wrong — throws ArgumentException if the key is a duplicate:
prices.Add("SKU-100", 19.99m);
prices.Add("SKU-100", 24.99m); // ArgumentException — key already existsCorrect — use the indexer, which overwrites instead of throwing, when you want "insert-or-update" behavior:
prices["SKU-100"] = 24.99m; // overwrites cleanlyWriting code that assumes entries come back in insertion order, then being surprised when a future .NET version (or a removal) changes that order. If order matters, sort explicitly, or keep a separate ordered list of keys.
List<T>.HashSet<T> (next lesson).List<KeyValuePair<...>> or a different design.TryGetValue for safety, indexer only when you're sure the key exists.TryGetValue to look up safely; use the indexer (dict[key] = value) to insert or overwrite.You've seen how dictionaries use hashing to make key-based lookups fast. Let's test your understanding.
1. Why is finding a product by SKU in a Dictionary<string, Product> typically much faster than finding it in a List<Product>?
Correct: B
Why B is correct: Hashing lets the dictionary compute exactly where a key's data lives, avoiding the need to check every other entry — this is what makes lookups roughly constant-time even as the collection grows large.
Why A is incorrect: Dictionaries make no ordering guarantee at all, alphabetical or otherwise.
Why C is incorrect: Dictionaries scale to millions of entries while keeping lookups fast — that's the entire point of hashing.
Why D is incorrect: The difference is dramatic and grows with the size of the collection — a list search visits, on average, half its items; a dictionary lookup does not.
Reinforcement: Hashing is the mechanism, and "jump straight to the bucket" is the payoff.
2. What happens when you access prices["SKU-999"] using the indexer, but no entry with that key exists?
Correct: C
Why C is correct: Reading via the indexer requires the key to exist. If it doesn't, the dictionary throws KeyNotFoundException rather than guessing or returning a placeholder value.
Why A is incorrect: null is never silently returned for a missing key on a read — that would hide a real bug.
Why B is incorrect: There's no automatic default-value fallback when reading with the indexer.
Why D is incorrect: Reading with the indexer never creates entries — only writing to it does (e.g. prices["new"] = 5;).
Reinforcement: Use TryGetValue whenever a key might be missing, so you can handle that case explicitly instead of catching an exception.
3. Why might two different keys occasionally end up needing to be compared with Equals() inside the same bucket?
Correct: B
Why B is correct: A hash function can map different inputs to the same bucket index — this is called a collision. When it happens, the dictionary keeps a short chain of entries in that bucket and uses Equals() to find the exact match among them.
Why A is incorrect: Buckets aren't sorted alphabetically — bucket placement comes from the hash code, not the key's natural ordering.
Why C is incorrect: A dictionary distributes keys across many buckets specifically to avoid this — a single shared bucket would make it no faster than a list.
Why D is incorrect: Collisions are a normal, expected part of how hashing works — a good hash function just keeps them rare and the resulting chains short.
Reinforcement: Collisions are handled gracefully by dictionaries; they don't break correctness, they just occasionally require one extra comparison.
4. A team is deduplicating a huge log of visitor IDs and, for each ID, wants to store the timestamp of that visitor's first visit. Which collection fits best?
Correct: B
Why B is correct: Each visitor ID is a natural unique key, and each needs an associated value (the first-visit timestamp) — exactly the key/value shape a dictionary is designed for, with fast lookups to check "have I seen this ID before?"
Why A is incorrect: A plain list of IDs has nowhere to store the associated timestamp, and checking for duplicates would require a slow scan for every new log entry.
Why C is incorrect: The number of unique visitors isn't known ahead of time, ruling out a fixed-size array.
Why D is incorrect: A single concatenated string would make it essentially impossible to look up or update individual visitor data efficiently.
Reinforcement: Whenever you need "given this ID, tell me its associated data, fast," reach for a dictionary.
You now understand how dictionaries turn slow scans into near-instant lookups. Next: guaranteeing uniqueness with HashSet<T> — a close cousin that uses the very same hashing trick.
dotnetmadeeasy.com — Learn C# and .NET, the right way.