Lesson 117 min read

Collections & Generics

The right container for every job

Beyond Arrays and Lists

You already know List<T>, but C# has a whole toolbox of collections, each designed for a specific job. Picking the right one is like choosing the right container in your kitchen:

  • Dictionary<K,V> — a labeled filing cabinet powered by hash functions. Look things up instantly by key.
  • HashSet<T> — a bag of unique items. No duplicates allowed. Fast checks for "is this in here?"
  • Queue<T> — a line at a store. First in, first out (FIFO).
  • Stack<T> — a stack of plates. Last in, first out (LIFO).

The <T> part is called a generic type parameter. It means "you decide what type goes in here." Generics give you type safety without writing separate code for each type.

Dictionary<K,V> — Key-Value Lookup

// Create a dictionary: country code → country name
var countries = new Dictionary<string, string>
{
["US"] = "United States",
["JP"] = "Japan",
["BR"] = "Brazil"
};
// Add more entries
countries["DE"] = "Germany";
countries["IN"] = "India";
// Look up by key
Console.WriteLine($"JP = {countries["JP"]}");
// Safe lookup (avoid KeyNotFoundException)
if (countries.TryGetValue("FR", out string? name))
Console.WriteLine($"FR = {name}");
else
Console.WriteLine("FR not found!");
// Check if key exists
Console.WriteLine($"Has US? {countries.ContainsKey("US")}");
// Iterate over all pairs
Console.WriteLine($"\nAll {countries.Count} countries:");
foreach (var pair in countries)
Console.WriteLine($" {pair.Key}{pair.Value}");
Output
JP = Japan
FR not found!
Has US? True

All 5 countries:
  US → United States
  JP → Japan
  BR → Brazil
  DE → Germany
  IN → India

HashSet<T> — Unique Items Only

var visited = new HashSet<string> { "Paris", "Tokyo", "London" };
// Add — returns false if already exists
bool added1 = visited.Add("Sydney"); // true — new city
bool added2 = visited.Add("Paris"); // false — already there!
Console.WriteLine($"Added Sydney: {added1}");
Console.WriteLine($"Added Paris again: {added2}");
// Super fast contains check
Console.WriteLine($"Been to Tokyo? {visited.Contains("Tokyo")}");
// Set operations
var wishlist = new HashSet<string> { "Rome", "Tokyo", "Cairo" };
// Intersection — cities in BOTH sets
var both = new HashSet<string>(visited);
both.IntersectWith(wishlist);
Console.Write("In both: ");
foreach (var city in both) Console.Write($"{city} ");
Console.WriteLine();
// Except — in wishlist but not visited
wishlist.ExceptWith(visited);
Console.Write("Still need to visit: ");
foreach (var city in wishlist) Console.Write($"{city} ");
Console.WriteLine();
Output
Added Sydney: True
Added Paris again: False
Been to Tokyo? True
In both: Tokyo 
Still need to visit: Rome Cairo 

Queue & Stack — Order Matters

Queue<T> and Stack<T> control the order things come out:

  • Queue (FIFO): Like a line at the movies. First person in line is first to get served. Use Enqueue() to add, Dequeue() to remove.
  • Stack (LIFO): Like a stack of pancakes. The last pancake added is the first one eaten. Use Push() to add, Pop() to remove.

Both have a Peek() method that lets you look at the next item without removing it.

Queue<T> and Stack<T>

// Queue — first in, first out
var printQueue = new Queue<string>();
printQueue.Enqueue("Report.pdf");
printQueue.Enqueue("Photo.jpg");
printQueue.Enqueue("Resume.docx");
Console.WriteLine($"Next to print: {printQueue.Peek()}");
Console.WriteLine($"Queue size: {printQueue.Count}");
while (printQueue.Count > 0)
{
string job = printQueue.Dequeue();
Console.WriteLine($" Printing: {job}");
}
// Stack — last in, first out
var undoStack = new Stack<string>();
undoStack.Push("Typed 'Hello'");
undoStack.Push("Made text bold");
undoStack.Push("Changed font to Arial");
Console.WriteLine($"\nLast action: {undoStack.Peek()}");
Console.WriteLine("Undoing:");
while (undoStack.Count > 0)
{
string action = undoStack.Pop();
Console.WriteLine($" Undo: {action}");
}
Output
Next to print: Report.pdf
Queue size: 3
  Printing: Report.pdf
  Printing: Photo.jpg
  Printing: Resume.docx

Last action: Changed font to Arial
Undoing:
  Undo: Changed font to Arial
  Undo: Made text bold
  Undo: Typed 'Hello'

Writing Your Own Generic Code

You can create your own generic classes and methods. The <T> is a placeholder that gets replaced with a real type when someone uses your code. It's like a recipe that says "take one item of type T" — the cook decides what T is.

Generic Methods & Classes

// Generic method — works with any type
static T GetFirst<T>(List<T> items)
{
if (items.Count == 0)
throw new InvalidOperationException("List is empty!");
return items[0];
}
// Generic class — a simple wrapper
class Pair<T1, T2>
{
public T1 First { get; }
public T2 Second { get; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
public override string ToString() => $"({First}, {Second})";
}
// Using the generic method
var names = new List<string> { "Alice", "Bob", "Charlie" };
var numbers = new List<int> { 42, 17, 99 };
Console.WriteLine($"First name: {GetFirst(names)}");
Console.WriteLine($"First number: {GetFirst(numbers)}");
// Using the generic class
var coordinate = new Pair<double, double>(40.7128, -74.0060);
var entry = new Pair<string, int>("Score", 100);
Console.WriteLine($"NYC: {coordinate}");
Console.WriteLine($"Entry: {entry}");
Output
First name: Alice
First number: 42
NYC: (40.7128, -74.006)
Entry: (Score, 100)
Note: 🗂️ Collection cheat sheet: Need to look up by key? → Dictionary. Need uniqueness? → HashSet. Need FIFO order? → Queue. Need LIFO/undo? → Stack. Need a flexible ordered list? → List. Choosing the right collection is half the battle!

Quick check

What happens if you try to add a duplicate value to a HashSet?

Continue reading

Hash FunctionsData Structure
Collisions, load factor, probing
Sets vs MapsData Structure
Use cases and tradeoffs
Dynamic ArraysData Structure
Resizing strategy, amortized O(1) push
Collections FrameworkJava
List, Set, Map — the power tools of Java
Dictionaries & SetsPython
Look things up by name instead of by number
Interfaces & Abstract ClassesLINQ