Skip to main content

πŸ“ 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) and Stack<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:

graph TD A["Need a collection?"] --> B["Look up by a key?
β†’ 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.

OperationList<T>Dictionary / HashSet
Find / look up an itemSlow β€” checks items one by oneFast β€” near-instant, regardless of size
Access by numeric indexFastNot supported (no index)
Keep insertion orderYesNot guaranteed
πŸ’‘ The practical rule: If your code loops through a List just to find something by a key or to check membership, a Dictionary or HashSet is 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 positionList<T>
Look things up by a keyDictionary<TKey, TValue>
Store unique items / test membership fastHashSet<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:

  1. Create a new project called WordCount.
  2. Start with a sentence and split it into words: string[] words = sentence.Split(' ');
  3. 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.
  4. Print each word and its count.
  5. 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; use TryGetValue/ContainsKey to avoid KeyNotFoundException.
  • 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 offer Peek.
  • 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

πŸš€ 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.