π Lesson 1.3: Collections in Depth
You know arrays and List<T>. But .NET offers specialized collections that are dramatically better for particular jobs β fast lookups, unique items, and ordered processing. Choosing the right one makes your code clearer and faster.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Use
Dictionary<TKey, TValue>for fast key-based lookups - Use
HashSet<T>to store unique items and test membership quickly - Use
Queue<T>(FIFO) andStack<T>(LIFO) for ordered processing - Explain why the right collection can be far faster than a
List - Choose the appropriate collection for a given problem
Estimated Time: 60 minutes
Project: Solve small problems by picking the best-fit collection.
In This Lesson
Beyond the List
A List<T> is a great general-purpose collection, but it's not always the best tool. Suppose you want to look up a person's phone number by their name. With a list you'd have to loop through every entry checking names β slow, and awkward to write.
The .NET class library (all generic, thanks to Lesson 1.2) includes collections designed for exactly these situations. Each is optimized for a specific access pattern:
β Dictionary"] A --> C["Only unique items?
β HashSet"] A --> D["First-in, first-out?
β Queue"] A --> E["Last-in, first-out?
β Stack"] A --> F["Ordered, indexed list?
β List"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style B fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style C fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style D fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style E fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style F fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
All of these live in System.Collections.Generic, which modern projects include automatically.
Dictionary: Key/Value Lookups
A Dictionary<TKey, TValue> stores pairs: each unique key maps to a value. Look up a value by its key directly β no looping. It's like a real dictionary: look up a word (key) to get its definition (value).
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Ada"] = 30; // add or update by key
ages["Grace"] = 45;
ages.Add("Alan", 41); // Add also works (throws if key exists)
Console.WriteLine(ages["Grace"]); // 45 β instant lookup, no loop
Console.WriteLine(ages.Count); // 3
β οΈ Missing keys throw
Reading a key that doesn't exist (ages["Bob"]) throws a KeyNotFoundException. Check first, or use TryGetValue β the safe pattern that mirrors TryParse:
if (ages.TryGetValue("Bob", out int bobAge))
{
Console.WriteLine($"Bob is {bobAge}.");
}
else
{
Console.WriteLine("Bob isn't in the dictionary.");
}
// Or just test membership:
if (ages.ContainsKey("Ada")) { /* ... */ }
Loop over a dictionary with foreach; each item is a KeyValuePair with .Key and .Value:
foreach (KeyValuePair<string, int> entry in ages)
{
Console.WriteLine($"{entry.Key} is {entry.Value}");
}
// You can also loop just the keys or just the values:
foreach (string name in ages.Keys) { Console.WriteLine(name); }
β Keys are unique
Each key appears at most once. Assigning to an existing key updates its value rather than adding a duplicate. This makes dictionaries perfect for lookups, counts, and caches.
HashSet: Unique Items
A HashSet<T> stores a collection of unique values β duplicates are silently ignored β and answers "is this item present?" extremely fast.
HashSet<string> visitors = new HashSet<string>();
visitors.Add("Ada");
visitors.Add("Grace");
Console.WriteLine(visitors.Add("Ada")); // False β already present, not added
Console.WriteLine(visitors.Count); // 2 (no duplicate)
Console.WriteLine(visitors.Contains("Grace")); // True β fast membership test
A common use is de-duplicating: drop a list into a set and the duplicates disappear.
List<int> numbers = new List<int> { 1, 2, 2, 3, 3, 3 };
HashSet<int> unique = new HashSet<int>(numbers);
Console.WriteLine(unique.Count); // 3
π‘ Sets support set math
HashSet offers real set operations: UnionWith (combine), IntersectWith (items in both), and ExceptWith (remove items found in another). Handy for comparing groups.
Queue and Stack
These two hold items in a specific processing order.
Queue<T> β First In, First Out (FIFO)
Like a line at a shop: the first person to join is the first served. Add with Enqueue, remove the oldest with Dequeue.
Queue<string> line = new Queue<string>();
line.Enqueue("Ada");
line.Enqueue("Grace");
line.Enqueue("Alan");
Console.WriteLine(line.Dequeue()); // Ada (first in, first out)
Console.WriteLine(line.Peek()); // Grace (next up, without removing)
Console.WriteLine(line.Count); // 2
Stack<T> β Last In, First Out (LIFO)
Like a stack of plates: the last one placed is the first taken. Add with Push, remove the newest with Pop.
Stack<string> history = new Stack<string>();
history.Push("page1");
history.Push("page2");
history.Push("page3");
Console.WriteLine(history.Pop()); // page3 (last in, first out)
Console.WriteLine(history.Peek()); // page2 (top, without removing)
π‘ Where you'll see them
- Queue: processing jobs in arrival order, print queues, breadth-first search.
- Stack: undo/redo, browser back button, evaluating expressions.
Both throw InvalidOperationException if you Dequeue/Pop when empty β check Count first (or catch it, per Lesson 1.1).
Why the Right Choice Is Faster
This isn't just about tidy code β it's about speed. Finding an item in a List means potentially checking every element. A Dictionary or HashSet jumps almost straight to it, using a technique called hashing.
| Operation | List<T> | Dictionary / HashSet |
|---|---|---|
| Find / look up an item | Slow β checks items one by one | Fast β near-instant, regardless of size |
| Access by numeric index | Fast | Not supported (no index) |
| Keep insertion order | Yes | Not guaranteed |
π‘ The practical rule: If your code loops through aListjust to find something by a key or to check membership, aDictionaryorHashSetis almost certainly the better choice β often dramatically faster as the data grows.
β οΈ Trade-off: no order or index
The speed of dictionaries and sets comes from not maintaining a positional order. Don't rely on the order of items when iterating them, and remember you can't do set[0]. If you need ordered, indexable data, stick with a List.
Choosing a Collection
A quick decision guide you can keep handy:
| If you need to⦠| Use |
|---|---|
| Keep an ordered list you access by position | List<T> |
| Look things up by a key | Dictionary<TKey, TValue> |
| Store unique items / test membership fast | HashSet<T> |
| Process items in arrival order (FIFO) | Queue<T> |
| Process most-recent items first (LIFO) | Stack<T> |
β Start simple, switch when needed
It's fine to reach for a List first. But the moment you notice you're looping to find-by-key, deduplicating, or processing in a strict order, switch to the collection built for it. Your code gets shorter and faster.
Exercise & Quiz
ποΈ Exercise: Word Frequency Counter
Objective: Pick the right collection to count how often each word appears β a perfect job for a Dictionary.
Instructions:
- Create a new project called
WordCount. - Start with a sentence and split it into words:
string[] words = sentence.Split(' '); - Use a
Dictionary<string, int>to count each word: if the word is already a key, increment its count; otherwise add it with count 1. - Print each word and its count.
- Bonus: Use a
HashSet<string>to also report how many distinct words there were.
Starter Code:
string sentence = "the cat sat on the mat the cat purred";
string[] words = sentence.Split(' ');
Dictionary<string, int> counts = new Dictionary<string, int>();
// TODO: loop over words, count each into the dictionary
// TODO: print each word and its count
π‘ Hint
Inside the loop, use TryGetValue or ContainsKey: if present, counts[word]++;; otherwise counts[word] = 1;. A shortcut works too: counts[word] = counts.GetValueOrDefault(word) + 1;.
β Solution
string sentence = "the cat sat on the mat the cat purred";
string[] words = sentence.Split(' ');
Dictionary<string, int> counts = new Dictionary<string, int>();
foreach (string word in words)
{
if (counts.ContainsKey(word))
{
counts[word]++;
}
else
{
counts[word] = 1;
}
}
foreach (KeyValuePair<string, int> entry in counts)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}
// Bonus: distinct word count
HashSet<string> distinct = new HashSet<string>(words);
Console.WriteLine($"Distinct words: {distinct.Count}");
Output:
the: 3
cat: 2
sat: 1
on: 1
mat: 1
purred: 1
Distinct words: 6
π― Quick Quiz
Question 1: Which collection is best for looking up a value by a unique key?
Question 2: What happens if you Add a value that's already in a HashSet?
Question 3: A Queue<T> processes items in what order?
Summary
π Key Takeaways
Dictionary<TKey, TValue>maps unique keys to values for fast lookups; useTryGetValue/ContainsKeyto avoidKeyNotFoundException.HashSet<T>stores unique items and tests membership fast; great for de-duplication and set math.Queue<T>is FIFO (Enqueue/Dequeue);Stack<T>is LIFO (Push/Pop). Both offerPeek.- Dictionaries and sets are far faster for lookups than looping a
Listβ but don't guarantee order or support indexing. - Match the collection to the access pattern: index β List, key β Dictionary, uniqueness β HashSet, order β Queue/Stack.
π Additional Resources
- Dictionary<TKey,TValue> β API reference
- HashSet<T> β API reference
- Collections and Data Structures β overview
π What's Next?
That completes Module 1 β you can write robust, reusable, well-structured code. In Module 2, C# gets expressive. First up, Lesson 2.1: Delegates and Lambdas, where you'll learn to treat behavior itself as data you can pass around.
π Module 1 complete!
Exceptions, generics, and the right collections β that's a robust foundation. Next, we make C# expressive.