C# Collections Overview
Introduction
Collections are a fundamental concept in C# programming, providing various ways to store, organize, and manipulate groups of related objects. Unlike arrays that have a fixed size, most C# collections can dynamically grow and shrink as needed, offering more flexibility in how you manage data in your applications.
In this guide, we'll explore the different types of collections available in C#, when to use each type, and how they can make your code more efficient and maintainable.
What are Collections?
Collections in C# are specialized classes designed to store, manage, and manipulate groups of related objects. They provide methods and properties to add, remove, find, and iterate through items in the collection.
All collection classes in C# are part of the System.Collections
namespaces:
System.Collections
: Contains non-generic collection classesSystem.Collections.Generic
: Contains generic collection classes (type-safe)System.Collections.Specialized
: Contains specialized collection classesSystem.Collections.Concurrent
: Contains thread-safe collection classes for parallel programming
Types of Collections in C#
1. Arrays
Arrays are the most basic collection type in C#. They store a fixed number of elements of the same type and are indexed starting from 0.
// Declaring and initializing an array
int[] numbers = new int[5]; // An array of 5 integers
numbers[0] = 10;
numbers[1] = 20;
// Another way to initialize an array
string[] fruits = { "Apple", "Banana", "Orange" };
// Accessing elements
Console.WriteLine(fruits[1]); // Output: Banana
// Iterating through an array
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
// Output:
// Apple
// Banana
// Orange
Key characteristics of arrays:
- Fixed size (cannot be changed after creation)
- Fast access to elements using index
- Elements are stored contiguously in memory
2. List<T>
List<T>
is a generic collection that represents a strongly-typed list of objects that can be accessed by index. It provides methods to search, sort, and manipulate lists.
// Creating a List
List<int> numberList = new List<int>();
// Adding elements
numberList.Add(10);
numberList.Add(20);
numberList.Add(30);
// Adding multiple elements at once
numberList.AddRange(new int[] { 40, 50 });
// Accessing elements
Console.WriteLine(numberList[2]); // Output: 30
// Removing elements
numberList.Remove(20);
numberList.RemoveAt(0); // Removes element at index 0
// Checking if an element exists
bool contains = numberList.Contains(30); // true
// Finding elements
int firstGreaterThan30 = numberList.Find(x => x > 30); // 40
// Iterating through a list
foreach (int number in numberList)
{
Console.WriteLine(number);
}
// Output:
// 30
// 40
// 50
Key characteristics of List<T>
:
- Dynamic size (grows as needed)
- Strong typing with generics
- Rich set of methods for data manipulation
- Efficient for searching, sorting, and data access operations
3. Dictionary<TKey, TValue>
Dictionary<TKey, TValue>
represents a collection of key/value pairs that are organized based on the key. It provides fast lookups by key.
// Creating a Dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();
// Adding key/value pairs
ages.Add("John", 25);
ages.Add("Sarah", 30);
ages.Add("Mike", 35);
// Another way to add or update values
ages["Emma"] = 28;
// Accessing values
Console.WriteLine(ages["Sarah"]); // Output: 30
// Checking if a key exists
if (ages.ContainsKey("John"))
{
Console.WriteLine($"John's age is {ages["John"]}");
}
// Safe way to get a value
if (ages.TryGetValue("David", out int davidsAge))
{
Console.WriteLine(davidsAge);
}
else
{
Console.WriteLine("David not found in dictionary");
}
// Iterating through a dictionary
foreach (KeyValuePair<string, int> pair in ages)
{
Console.WriteLine($"{pair.Key} is {pair.Value} years old");
}
// Output:
// John is 25 years old
// Sarah is 30 years old
// Mike is 35 years old
// Emma is 28 years old
Key characteristics of Dictionary<TKey, TValue>
:
- Fast lookups using keys (O(1) complexity)
- Keys must be unique
- Unordered collection
- Efficient for frequent lookups by key
4. HashSet<T>
HashSet<T>
represents a set of unique values. It's optimized for fast lookups and determining whether an element exists in the set.
// Creating a HashSet
HashSet<string> uniqueNames = new HashSet<string>();
// Adding elements
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
uniqueNames.Add("Charlie");
// Adding a duplicate (this will be ignored)
uniqueNames.Add("Alice");
// Checking if an element exists
bool containsBob = uniqueNames.Contains("Bob"); // true
// Set operations
HashSet<string> moreNames = new HashSet<string> { "Charlie", "Dave", "Eve" };
// Union (combines both sets)
uniqueNames.UnionWith(moreNames);
// Printing all names
foreach (string name in uniqueNames)
{
Console.WriteLine(name);
}
// Output:
// Alice
// Bob
// Charlie
// Dave
// Eve
Key characteristics of HashSet<T>
:
- Contains only unique elements
- Very fast lookups, additions, and removals
- Excellent for removing duplicates from a collection
- Provides set operations (union, intersection, difference)
5. Queue<T>
Queue<T>
represents a first-in, first-out (FIFO) collection of objects.
// Creating a Queue
Queue<string> printQueue = new Queue<string>();
// Adding items to the queue (Enqueue)
printQueue.Enqueue("Document 1");
printQueue.Enqueue("Document 2");
printQueue.Enqueue("Document 3");
// Viewing the item at the beginning of the queue without removing it
string nextToPrint = printQueue.Peek(); // "Document 1"
Console.WriteLine($"Next document to print: {nextToPrint}");
// Removing and returning the first item (Dequeue)
string currentlyPrinting = printQueue.Dequeue(); // "Document 1"
Console.WriteLine($"Currently printing: {currentlyPrinting}");
Console.WriteLine($"Items remaining in queue: {printQueue.Count}");
// Processing a queue
while (printQueue.Count > 0)
{
Console.WriteLine($"Printing: {printQueue.Dequeue()}");
}
// Output:
// Next document to print: Document 1
// Currently printing: Document 1
// Items remaining in queue: 2
// Printing: Document 2
// Printing: Document 3
Key characteristics of Queue<T>
:
- FIFO (First-In-First-Out) data structure
- Useful for processing items in sequence
- Common in task scheduling scenarios
6. Stack<T>
Stack<T>
represents a last-in, first-out (LIFO) collection of objects.
// Creating a Stack
Stack<string> browserHistory = new Stack<string>();
// Adding items to the stack (Push)
browserHistory.Push("https://www.google.com");
browserHistory.Push("https://www.github.com");
browserHistory.Push("https://www.stackoverflow.com");
// Viewing the item at the top of the stack without removing it
string currentPage = browserHistory.Peek(); // "https://www.stackoverflow.com"
Console.WriteLine($"Current page: {currentPage}");
// Removing and returning the top item (Pop)
string previousPage = browserHistory.Pop(); // "https://www.stackoverflow.com"
Console.WriteLine($"Going back to: {browserHistory.Peek()}");
// Processing a stack
Console.WriteLine("Browser history (most recent first):");
while (browserHistory.Count > 0)
{
Console.WriteLine(browserHistory.Pop());
}
// Output:
// Current page: https://www.stackoverflow.com
// Going back to: https://www.github.com
// Browser history (most recent first):
// https://www.github.com
// https://www.google.com
Key characteristics of Stack<T>
:
- LIFO (Last-In-First-Out) data structure
- Useful for tracking state that needs to be unwound in reverse order
- Common in undo mechanisms, expression evaluation, and algorithm implementations
Choosing the Right Collection
Selecting the appropriate collection type depends on your specific needs:
-
Use arrays when you need a fixed-size collection with fast access by index.
-
Use
List<T>
when you need a dynamic collection that grows as needed with index access. -
Use
Dictionary<TKey, TValue>
when you need fast lookups based on a key. -
Use
HashSet<T>
when you need to ensure uniqueness and perform set operations. -
Use
Queue<T>
when you need to process items in the order they were added (FIFO). -
Use
Stack<T>
when you need to process items in reverse order of addition (LIFO).
Real-World Application: Task Management System
Let's build a simple task management system to demonstrate how different collections can work together:
using System;
using System.Collections.Generic;
public class Task
{
public int Id { get; set; }
public string Description { get; set; }
public bool IsCompleted { get; set; }
}
public class TaskManager
{
// Store all tasks with fast lookup by ID
private Dictionary<int, Task> _tasks = new Dictionary<int, Task>();
// Queue for tasks to be processed in order
private Queue<int> _taskQueue = new Queue<int>();
// Set of completed task IDs for quick checking
private HashSet<int> _completedTasks = new HashSet<int>();
// Last added task IDs for undo functionality
private Stack<int> _lastAddedTasks = new Stack<int>();
public void AddTask(int id, string description)
{
var task = new Task { Id = id, Description = description, IsCompleted = false };
_tasks.Add(id, task);
_taskQueue.Enqueue(id);
_lastAddedTasks.Push(id);
Console.WriteLine($"Task {id} added: {description}");
}
public void CompleteNextTask()
{
if (_taskQueue.Count == 0)
{
Console.WriteLine("No tasks in the queue!");
return;
}
int id = _taskQueue.Dequeue();
_tasks[id].IsCompleted = true;
_completedTasks.Add(id);
Console.WriteLine($"Completed task {id}: {_tasks[id].Description}");
}
public bool IsTaskCompleted(int id)
{
return _completedTasks.Contains(id);
}
public void UndoLastAddedTask()
{
if (_lastAddedTasks.Count == 0)
{
Console.WriteLine("Nothing to undo!");
return;
}
int id = _lastAddedTasks.Pop();
// Remove from other collections if present
_tasks.Remove(id);
_completedTasks.Remove(id);
// Note: Can't easily remove from a queue, so we'll just skip it when processing
Console.WriteLine($"Removed task {id} (undo operation)");
}
public void ShowAllTasks()
{
Console.WriteLine("\nAll Tasks:");
foreach (var task in _tasks.Values)
{
string status = task.IsCompleted ? "Completed" : "Pending";
Console.WriteLine($"ID: {task.Id}, Description: {task.Description}, Status: {status}");
}
}
}
public class Program
{
public static void Main()
{
TaskManager manager = new TaskManager();
// Add some tasks
manager.AddTask(1, "Write documentation");
manager.AddTask(2, "Fix bugs in login module");
manager.AddTask(3, "Implement new feature");
manager.AddTask(4, "Code review PR #123");
// Process tasks
manager.CompleteNextTask(); // Completes task 1
manager.CompleteNextTask(); // Completes task 2
// Check status
Console.WriteLine($"Is task 2 completed? {manager.IsTaskCompleted(2)}"); // True
Console.WriteLine($"Is task 3 completed? {manager.IsTaskCompleted(3)}"); // False
// Undo the last added task
manager.UndoLastAddedTask(); // Removes task 4
// Show all tasks
manager.ShowAllTasks();
}
}
/* Output:
Task 1 added: Write documentation
Task 2 added: Fix bugs in login module
Task 3 added: Implement new feature
Task 4 added: Code review PR #123
Completed task 1: Write documentation
Completed task 2: Fix bugs in login module
Is task 2 completed? True
Is task 3 completed? False
Removed task 4 (undo operation)
All Tasks:
ID: 1, Description: Write documentation, Status: Completed
ID: 2, Description: Fix bugs in login module, Status: Completed
ID: 3, Description: Implement new feature, Status: Pending
*/
In this example, we've used:
Dictionary<int, Task>
to store and quickly access tasks by their IDQueue<int>
to process tasks in the order they were addedHashSet<int>
to efficiently check if a task is completedStack<int>
to implement an undo feature for the last added tasks
Summary
C# provides a rich set of collection classes to handle different scenarios in your applications:
Arrays
for fixed-size collections with fast indexingList<T>
for dynamic, index-accessible collectionsDictionary<TKey, TValue>
for key-based lookupsHashSet<T>
for unique elements and set operationsQueue<T>
for first-in, first-out processingStack<T>
for last-in, first-out processing
Understanding when to use each collection type will help you write more efficient and maintainable code. The choice of collection can significantly impact your application's performance and readability.
Exercises
-
Create a program that reads a list of words from user input and counts the frequency of each word using the appropriate collection.
-
Implement a simple browser navigation system with "back" and "forward" functionality using two stacks.
-
Create a customer service ticketing system that processes requests in order of arrival, using an appropriate collection.
-
Write a program that removes duplicates from a list of integers and then sorts them in ascending order.
Additional Resources
- Microsoft Documentation: Collections (C#)
- C# Collection Types
- Performance Considerations for C# Collections
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)