Lesson 4 of 45 beginner

Collections: Picking the Right Container

The wrong container turns a fast loop into a slow one

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

You check Contains for 100,000 ids against a collection of 100,000 ids. Which collection is fastest?

Open interactive version (quiz + challenge)

Real-world analogy

Think of finding a book. In a pile on the floor you check each book until you find it. In a library with an index card, you look up the name and walk straight to the shelf. A List is the pile, and a Dictionary is the card index. Here the number of books checked is the cost, and the container decides it.

What is it?

A collection is a container for many values, and each kind trades speed for features. Lists keep order and dictionaries look up by key. Sets keep unique values, and queues and stacks control the order of removal. Picking by how you will use the data, not by habit, often removes a whole class of slow code.

Real-world relevance

A report job loops over 50,000 orders and for each one calls blockedUsers.Contains(order.UserId) on a list. That is 50,000 scans of the list. Changing the list to a HashSet turns each check into one lookup. A job that took minutes finishes in seconds.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

What happens when you call list.Remove(x) inside a foreach over the same list?

A public method returns your internal List<Order>. What is the safest return type?

Code example

public record User(int Id, string Name);
public record Order(int UserId, decimal Total);

public static class Report
{
    // Slow: scans both lists for every order
    public static void Slow(List<Order> orders, List<User> users, List<int> blocked)
    {
        foreach (var order in orders)
        {
            if (blocked.Contains(order.UserId)) continue;
            var user = users.First(u => u.Id == order.UserId);
            Console.WriteLine($"{user.Name}: {order.Total}");
        }
    }

    // Fast: build the lookups once, then each check is one hash lookup
    public static void Fast(List<Order> orders, List<User> users, List<int> blocked)
    {
        var blockedIds = new HashSet<int>(blocked);
        var usersById = users.ToDictionary(u => u.Id);

        foreach (var order in orders)
        {
            if (blockedIds.Contains(order.UserId)) continue;
            if (usersById.TryGetValue(order.UserId, out var user))
                Console.WriteLine($"{user.Name}: {order.Total}");
        }
    }
}

Line-by-line walkthrough

  1. 1. The records User and Order are small data shapes used by both methods.
  2. 2. Slow calls blocked.Contains for every order. That scans the list each time.
  3. 3. Slow also calls users.First for every order, which scans the users list each time. The work grows as orders times users.
  4. 4. Fast builds a HashSet of blocked ids once, and a Dictionary of users by id once.
  5. 5. Inside the loop, blockedIds.Contains and usersById.TryGetValue are each one hash lookup.
  6. 6. TryGetValue also handles the missing-user case without throwing.

Spot the bug

var orders = new List<Order>
{
    new(1, "paid"), new(2, "cancelled"), new(3, "paid")
};

foreach (var order in orders)
{
    if (order.Status == "cancelled")
        orders.Remove(order);
}

public record Order(int Id, string Status);
Need a hint?
What happens to the list while the loop is still walking it?
Show answer
Changing the list inside foreach invalidates the enumerator, so .NET throws InvalidOperationException ("Collection was modified"). Fix: use <code>orders.RemoveAll(o => o.Status == "cancelled")</code>, or loop over <code>orders.ToList()</code> and remove from the original.

Explain like I'm 5

If your toys are in one big pile, you search the whole pile each time. If every toy has a labelled box, you go straight to the right one. Picking the right box makes finding things fast.

Fun fact

A Dictionary does not keep a promised order. It often looks sorted by insertion, but the docs do not guarantee it, so never rely on it.

Hands-on challenge

Say it out loud in 30 seconds: "When would you use a Dictionary or HashSet instead of a List, and why is it faster?" Then check: did you mention O(1) lookup, the hash code, and the cost of scanning a list?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook