← Open in the full interactive course (progress tracking, search & more)

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.

What Is It?

The Simple Explanation

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.

The Technical Definition

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.

Dictionary = Key + Value, Looked Up Fast
Dictionary<string, decimal> prices = new() { ["SKU-100"] = 19.99m, ["SKU-200"] = 4.75m, }; decimal price = prices["SKU-100"]; // 19.99 — instant lookup, not a scan

Why Does It Exist?

The Problem

Suppose 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.

The Need

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.

The Solution

Dictionary<string, Product> catalog = BuildCatalog(); Product found = catalog["SKU-4821"]; // near-instant, regardless of catalog size

The 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.

Big Picture

LIST SEARCH vs DICTIONARY LOOKUP
List<Product>.Find(...)
Check item 1 → no match
Check item 2 → no match
Check item 3 → no match
... keeps scanning ...
Check item 4821 → found
Dictionary["SKU-4821"]
Hash the key

Jump straight to its bucket

found — no scanning at all
A list search gets slower as it grows. A dictionary lookup stays roughly constant — that's the entire point of hashing.

How It Works

USING A DICTIONARY, STEP BY STEP
1. CREATE AND ADD ENTRIES
Dictionary<string, decimal> prices = [];
prices.Add("SKU-100", 19.99m);
prices["SKU-200"] = 4.75m;   // indexer syntax also adds a new entry
2. LOOK UP A VALUE BY KEY
decimal price = prices["SKU-100"]; // 19.99
decimal missing = prices["SKU-999"]; //  throws KeyNotFoundException!
3. CHECK SAFELY WITH TryGetValue
if (prices.TryGetValue("SKU-999", out decimal foundPrice))
    Console.WriteLine($"Price: {foundPrice}");
else
    Console.WriteLine("Not found.");
4. ITERATE OVER KEYS, VALUES, OR PAIRS
foreach (KeyValuePair<string, decimal> pair in prices)
    Console.WriteLine($"{pair.Key}: {pair.Value:C}");

foreach (string sku in prices.Keys)
    Console.WriteLine(sku);

Simple Example

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: 1

Code → Meaning → Result:

Real-World Example

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.

Analogy

A Building's Mailroom, Sorted by Apartment Number

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.

Under the Hood

WHY DICTIONARY LOOKUPS ARE SO FAST — HASHING
1. EVERY KEY HAS A HASH CODE
2. THE HASH CODE PICKS A BUCKET
3. COLLISIONS — WHEN TWO KEYS SHARE A BUCKET
4. WHY THIS IS CALLED "O(1)" — AVERAGE CONSTANT TIME

Common Confusion

1. A Dictionary vs a List of Key/Value Pairs

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.

2. Dictionaries don't guarantee order

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.

3. Indexer [] vs TryGetValue

dict[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.

Common Mistakes

Mistake 1 — Using the indexer without checking existence first

Wrong — throws if the key isn't there:

decimal price = prices["SKU-DOES-NOT-EXIST"]; // KeyNotFoundException

Correct:

if (prices.TryGetValue("SKU-DOES-NOT-EXIST", out decimal price)) Console.WriteLine(price); else Console.WriteLine("Not found.");

Mistake 2 — Calling 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 exists

Correct — use the indexer, which overwrites instead of throwing, when you want "insert-or-update" behavior:

prices["SKU-100"] = 24.99m; // overwrites cleanly

Mistake 3 — Relying on dictionary iteration order

Writing 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.

When Should I Use It?

Use Dictionary<TKey, TValue> when

Reach for something else when

Mental Model

Dictionary = a set of key/value pairs, addressed by key
Hashing = converting a key into a "which bucket" number
Lookup = hash the key → jump to its bucket → done (average O(1))

Remember:
· Keys are unique; values can repeat.
· TryGetValue for safety, indexer only when you're sure the key exists.
· No guaranteed iteration order.
· Dictionary lookups stay fast as the collection grows — list searches don't.

Key Takeaway


Check Your Understanding

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>?

Show answer

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?

Show answer

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?

Show answer

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?

Show answer

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.