C# Lists and Dictionaries

Use the generic collections you'll need every day: List<T> for ordered items that grow and shrink, Dictionary<TKey, TValue> for fast lookups by key, and HashSet<T> for unique values.

Lesson 10 of 43Intermediate17 min read

Arrays have a fixed size, but most real data changes. Users add items to a basket and remove them, new messages arrive, tasks get completed. The System.Collections.Generic namespace provides collections that grow and shrink as needed, and three of them cover most everyday work.

List<T> is an ordered list of items of type T that you can add to, remove from and sort, like a resizable array. Dictionary<TKey, TValue> stores key-value pairs, such as product codes and prices, and finds a value by its key almost instantly, however many items it holds. HashSet<T> stores unique values and answers "is this in the set?" very quickly.

The <T> part means these collections are generic: you choose the item type when you create one, and the compiler makes sure only that type goes in. In this lesson you'll use each collection for realistic tasks and learn which one fits which job.

List<T>

C#
var tasks = new List<string> { "Write report", "Email Sam" };
tasks.Add("Book train");            // add to the end
tasks.Insert(0, "Coffee");          // add at a position
tasks.Remove("Email Sam");          // remove by value
tasks.RemoveAt(0);                  // remove by index

Console.WriteLine(tasks.Count);     // 2
Console.WriteLine(tasks[0]);        // Write report
  • Count is the number of items (arrays use Length, lists use Count).
  • Contains, IndexOf, Find(predicate) and Exists(predicate) search the list.
  • Sort() sorts in place, and RemoveAll(predicate) removes every matching item.
  • List<string> names = []; is a short way to create an empty list in C# 12.

Dictionary<TKey, TValue>

C#
var prices = new Dictionary<string, decimal>
{
    ["apple"] = 0.50m,
    ["bread"] = 1.20m
};
prices["milk"] = 0.95m;            // add or replace
prices.Remove("apple");

if (prices.TryGetValue("bread", out decimal price))
{
    Console.WriteLine(price);       // 1.20
}
  • Keys are unique; setting prices["milk"] again replaces the value.
  • Reading a key that doesn't exist with prices["tea"] throws a KeyNotFoundException, so use TryGetValue or ContainsKey when the key might be missing.
  • Add(key, value) throws if the key already exists, which is useful when duplicates would be a bug.
  • Looping gives KeyValuePair items: foreach (var (name, cost) in prices).

HashSet, Queue and Stack

  • HashSet<T> ignores duplicates. Add returns false if the item was already there. UnionWith, IntersectWith and ExceptWith combine sets.
  • Queue<T> is first in, first out: Enqueue adds to the back and Dequeue takes from the front, like a line at a shop.
  • Stack<T> is last in, first out: Push and Pop, like an undo history.

Counting and grouping with a dictionary

Counting things is one of the most common jobs for a dictionary. GetValueOrDefault returns 0 for a missing int key, so you don't need an if to handle the first time you see a word:

C#
var counts = new Dictionary<string, int>();
foreach (string word in "the cat and the hat".Split(' '))
{
    counts[word] = counts.GetValueOrDefault(word) + 1;
}
Console.WriteLine(counts["the"]);   // 2

TryAdd(key, value) adds only when the key is new and returns false instead of throwing when it already exists, which is handy for "first one wins" rules.

A Dictionary makes no promise about the order of its items. To show results in a particular order, sort them as you loop:

C#
foreach (var (word, n) in counts.OrderByDescending(p => p.Value))
    Console.WriteLine($"{word}: {n}");

If the keys should always stay sorted, use SortedDictionary<TKey, TValue> instead. It keeps keys in order as you add them, at the cost of slightly slower lookups than a normal Dictionary.

Keys are compared exactly by default, so "Apple" and "apple" are different keys. Pass a comparer when you create the dictionary to ignore case:

C#
var stock = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

C# Collections Syntax

Collection syntax

var list = new List<int> { 3, 1, 2 };
list.Add(4); list.Remove(1); list.Sort();
int n = list.Count;  bool has = list.Contains(2);
int? big = list.Find(x => x > 2);
var dict = new Dictionary<string, int>();
dict["a"] = 1;  dict.ContainsKey("a");
if (dict.TryGetValue("a", out int v)) { }
foreach (var (key, value) in dict) { }
var set = new HashSet<string> { "x" };  set.Add("x");   // false
var q = new Queue<string>(); q.Enqueue("a"); q.Dequeue();
var s = new Stack<int>(); s.Push(1); s.Pop();

C# Collections Summary

  • List<T> is an ordered, resizable list: Add, Insert, Remove, Count, Sort, Find.
  • Dictionary<TKey, TValue> maps unique keys to values with fast lookups.
  • Use TryGetValue or ContainsKey when a key might be missing.
  • HashSet<T> stores unique items; Queue and Stack give FIFO and LIFO order.
  • Generic collections only accept the type you declare.

C# Collections Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A shopping basket with List<T>

C#
var basket = new List<string> { "Milk", "Bread", "Eggs" };

basket.Add("Apples");
basket.Insert(1, "Coffee");
basket.Remove("Bread");

Console.WriteLine($"{basket.Count} items: {string.Join(", ", basket)}");
Console.WriteLine(basket.Contains("Eggs"));
Console.WriteLine(basket.IndexOf("Apples"));

basket.Sort();
Console.WriteLine(string.Join(", ", basket));

int removed = basket.RemoveAll(item => item.StartsWith("C"));
Console.WriteLine($"Removed {removed}, left: {string.Join(", ", basket)}");
Output
4 items: Milk, Coffee, Eggs, Apples
True
3
Apples, Coffee, Eggs, Milk
Removed 1, left: Apples, Eggs, Milk
Line-by-line explanation
  1. Line 3Add puts an item at the end.
  2. Line 4Insert puts Coffee at index 1, pushing the others along.
  3. Line 5Remove deletes the first matching value.
  4. Line 9After the changes, Apples is at index 3.
  5. Line 14RemoveAll removes every item matching the condition and returns how many it removed.
Example 2

Counting words with a dictionary

C#
string text = "the cat sat on the mat the end";
var counts = new Dictionary<string, int>();

foreach (string word in text.Split(' '))
{
    if (counts.ContainsKey(word))
        counts[word]++;
    else
        counts[word] = 1;
}

foreach (var (word, count) in counts)
{
    if (count > 1)
        Console.WriteLine($"{word}: {count}");
}

Console.WriteLine($"Unique words: {counts.Count}");
Console.WriteLine(counts.TryGetValue("dog", out int dogs) ? dogs : 0);
Output
the: 3
Unique words: 6
0
Line-by-line explanation
  1. Lines 6 to 9Increase the count if the word was seen before, otherwise start it at 1.
  2. Line 12Deconstruction gives the key and value of each pair.
  3. Line 18Count on a dictionary is the number of keys: the, cat, sat, on, mat and end.
  4. Line 19TryGetValue returns false for a missing key instead of throwing.
Example 3

HashSet and Queue

C#
var visited = new HashSet<string>();
string[] pages = { "/home", "/shop", "/home", "/cart", "/shop" };

foreach (string page in pages)
{
    bool isNew = visited.Add(page);
    Console.WriteLine($"{page} {(isNew ? "first visit" : "seen before")}");
}
Console.WriteLine($"Unique pages: {visited.Count}");

var queue = new Queue<string>();
queue.Enqueue("Order 101");
queue.Enqueue("Order 102");
queue.Enqueue("Order 103");
Console.WriteLine($"Packing {queue.Dequeue()}, {queue.Count} waiting");
Console.WriteLine($"Next up: {queue.Peek()}");
Output
/home first visit
/shop first visit
/home seen before
/cart first visit
/shop seen before
Unique pages: 3
Packing Order 101, 2 waiting
Next up: Order 102
Line-by-line explanation
  1. Line 6Add returns false when the item is already in the set.
  2. Line 9Duplicates aren't stored, so only three pages remain.
  3. Line 15Dequeue removes and returns the item at the front, the oldest one.
  4. Line 16Peek looks at the front without removing it.

Common C# Collections Mistakes

Reading a missing key with the indexer

Don't
decimal p = prices["tea"];          // KeyNotFoundException
Do
decimal p = prices.TryGetValue("tea", out var found) ? found : 0m;

Searching a list when you need lookups

Don't
var user = users.Find(u => u.Id == id);   // checks every user, every time
Do
var usersById = users.ToDictionary(u => u.Id);
var user = usersById[id];                 // jumps straight to the key

Adding a duplicate key

Don't
stock.Add("pen", 10);
stock.Add("pen", 5);                // ArgumentException: same key already added
Do
stock["pen"] = 5;                   // replace
stock["pen"] = stock.GetValueOrDefault("pen") + 5;   // or add to it

C# Collections in Real Projects

Collections in real applications

Web APIs return List<T> results from databases, caches store objects in a Dictionary keyed by id, and permission systems keep a user's roles in a HashSet<string>. Background workers process jobs from a Queue<T>, and editors keep undo history in a Stack<T>. For thread-safe access, .NET also offers ConcurrentDictionary and channels with the same basic ideas.

C# Collections FAQ

What is the difference between an array and a List in C#?

An array has a fixed size set when it's created, while a List<T> grows and shrinks as you Add and Remove items. Arrays use Length and lists use Count. A list stores its items in an array internally and replaces it with a bigger one when it fills up, so it's nearly as fast for reading.

How do I check if a key exists in a Dictionary in C#?

Use dict.ContainsKey(key) if you only need a yes or no. If you also need the value, use dict.TryGetValue(key, out var value), which checks and reads in one lookup. Avoid reading dict[key] directly when the key might be missing, because it throws a KeyNotFoundException.

Does a C# Dictionary keep insertion order?

You shouldn't rely on it. A Dictionary<TKey, TValue> often appears to keep insertion order, but this isn't guaranteed and can change after items are removed. Sort with OrderBy when you display items, or use SortedDictionary<TKey, TValue> to keep keys sorted.

How do I remove duplicates from a list in C#?

Call list.Distinct().ToList() to get a new list with duplicates removed, keeping the first occurrence of each item. You can also build a HashSet<T> from the list with new HashSet<string>(list), which is useful when you want to keep the items unique from then on.

How do I make a Dictionary key case-insensitive?

Pass a comparer to the constructor: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Then "Apple", "APPLE" and "apple" all count as the same key for lookups, adds and ContainsKey.

C# Collections Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the code to add an item to the end of the list.

var names = new List<string>();
names.("Ada");
Predict the outputeasy

What does this print?

var nums = new List<int> { 5, 6, 7 };
nums.RemoveAt(0);
Console.WriteLine(nums[0]);
Multiple choicemedium

What happens when you read dict["missing"] for a key that doesn't exist?

Predict the outputmedium

What does this print?

var set = new HashSet<int> { 1, 2, 2, 3, 3, 3 };
Console.WriteLine(set.Count);
Multiple choicehard

Why is looking up a key in a Dictionary faster than searching a List?

C# Collections Quiz

5 questions to check what you learned.

  1. Which property gives the number of items in a List<T>?

    • Count
    • Length
    • Size
    • Total
  2. Can a dictionary have two items with the same key?

    • Yes
    • Only for strings
    • Only if the values differ
    • No, keys are unique
  3. Which collection is first in, first out?

    • Stack<T>
    • Queue<T>
    • HashSet<T>
    • Dictionary
  4. What does List.RemoveAll(predicate) return?

    • The new list
    • true or false
    • The number of items removed
    • Nothing
  5. What does HashSet.Add return when the item is already present?

    • false
    • true
    • It throws
    • The item