How to Choose the Right C# Collection

Date
Authors

Introduction

Collections are fundamental in C# for storing and managing groups of objects. Choosing the right collection type impacts your application's performance, memory usage, and maintainability. This guide covers all essential C# collections with clear examples, performance metrics, and practical use cases.

C# Collections Hierarchy

Here is the hierarchy of most important collections in C#:

C# Collections Hierarchy

1. List<T> – The Dynamic Array

A resizable array that automatically expands as needed.

When to Use

✅ Storing and accessing elements by index
✅ Frequent additions/removals at the end
✅ Sorting and searching

Code Example

List<string> fruits = new List<string>();

// Adding items
fruits.Add("Apple");
fruits.Add("Banana");

// Access by index
string firstFruit = fruits[0]; // "Apple"

// Iteration
foreach (var fruit in fruits)
{
    Console.WriteLine(fruit);
}

// Remove
fruits.Remove("Banana");

Performance

OperationComplexityReason for Complexity
Index AccessO(1)Direct array lookup by index.
SearchO(n)Linear scan required for unsorted data.
Insert/Remove (End)O(1)*Amortized constant time (may resize backing array).
Insert/Remove (Middle)O(n)Requires shifting elements.

2. Dictionary<TKey, TValue> – Fast Key-Value Lookups

A hash table that stores key-value pairs with O(1) lookups.

When to Use

✅ Fast lookups by unique keys
✅ No duplicate keys allowed
✅ Caching scenarios

Code Example

Dictionary<int, string> employees = new Dictionary<int, string>();

// Adding entries
employees.Add(101, "Alice");
employees.Add(102, "Bob");

// Lookup by key
if (employees.TryGetValue(101, out string employee))
{
    Console.WriteLine($"Found: {employee}"); // "Alice"
}

// Remove
employees.Remove(102);

Performance

OperationComplexityReason for Complexity
Key LookupO(1)Hash-based bucket access.
Insert/DeleteO(1)*Hashing + amortized resize cost.
Memory UsageHighStores buckets + entries + collision chains.

3. HashSet<T> – Unique Item Storage

A collection that enforces uniqueness with O(1) membership tests.

When to Use

✅ Ensuring no duplicates
✅ Fast Contains() checks
✅ Set operations (Union, Intersect)

Code Example

HashSet<string> tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

// Adding (duplicates auto-ignored)
tags.Add("C#");
tags.Add("c#"); // Not added

// Check existence
if (tags.Contains("C#"))
{
    Console.WriteLine("C# tag exists!");
}

// Set operations
var otherTags = new HashSet<string> { "Java", "C#" };
tags.IntersectWith(otherTags); // Now only "C#"

Performance

OperationComplexityReason for Complexity
AddO(1)Hash-based insertion.
ContainsO(1)Direct bucket check.
RemoveO(1)Hash-based deletion.

4. Queue<T> – First-In-First-Out (FIFO)

Processes items in the order they were added.

When to Use

✅ Task scheduling
✅ Message processing
✅ Breadth-first algorithms

Code Example

Queue<string> tasks = new Queue<string>();

// Enqueue
tasks.Enqueue("Process order");
tasks.Enqueue("Send email");

// Dequeue (process in order)
while (tasks.Count > 0)
{
    string currentTask = tasks.Dequeue();
    Console.WriteLine($"Processing: {currentTask}");
}

Performance

OperationComplexityReason for Complexity
EnqueueO(1)Fixed tail pointer update.
DequeueO(1)Fixed head pointer update.

5. Stack<T> – Last-In-First-Out (LIFO)

Processes the most recent item first.

When to Use

✅ Undo/redo functionality
✅ Depth-first algorithms
✅ Expression evaluation

Code Example

Stack<string> history = new Stack<string>();

// Push
history.Push("Homepage");
history.Push("Products");

// Pop (go back)
string lastPage = history.Pop(); // "Products"

Performance

OperationComplexityReason for Complexity
PushO(1)Single top pointer update.
PopO(1)Single top pointer update.

6. LinkedList<T> – Efficient Node Operations

A chain of nodes where each item points to the next and previous.

When to Use

✅ Frequent insertions/removals in the middle
✅ Implementing custom collections

Code Example

LinkedList<string> playlist = new LinkedList<string>();

// Adding songs
var first = playlist.AddFirst("Bohemian Rhapsody");
var last = playlist.AddLast("Sweet Child O'Mine");
playlist.AddAfter(first, "Hotel California");

// Traverse
var currentNode = playlist.First;
while (currentNode != null)
{
    Console.WriteLine(currentNode.Value);
    currentNode = currentNode.Next;
}

Performance

OperationComplexityReason for Complexity
Insert/Remove NodeO(1)Only node pointer updates needed.
Index AccessO(n)Requires traversal from head/tail.

7. SortedSet<T> – Auto-Sorted Unique Elements

A self-balancing binary search tree that maintains sorted order.

When to Use

✅ Maintaining sorted unique elements
✅ Range queries

Code Example

SortedSet<int> scores = new SortedSet<int>();

// Add (auto-sorts)
scores.Add(85);
scores.Add(90);
scores.Add(75);

// Get range (80-100)
foreach (var score in scores.GetViewBetween(80, 100))
{
    Console.WriteLine($"High score: {score}");
}

Performance

OperationComplexityReason for Complexity
Add/RemoveO(log n)Red-Black Tree rebalancing.
ContainsO(log n)Binary tree traversal.

Collection Comparison Table

CollectionIndex AccessSearchInsert/RemoveBest For
List<T>O(1)O(n)End: O(1), Middle: O(n)General-purpose storage
DictionaryTKey,TValue>N/AO(1)O(1)Key-value lookups
HashSet<T>N/AO(1)O(1)Unique items
Queue<T>N/AN/AO(1)FIFO processing
Stack<T>N/AN/AO(1)LIFO processing
LinkedList<T>O(n)O(n)O(1) at nodeFrequent middle changes
SortedSet<T>N/AO(log n)O(log n)Sorted unique items

Conclusion

Selecting the optimal collection depends on your specific needs:

  1. For Indexed Access: Use List<T> when you need fast positional access
  2. For Key-Value Pairs: Dictionary<TKey,TValue> provides O(1) lookups
  3. For Unique Items: HashSet<T> ensures uniqueness with fast membership checks
  4. For Ordered Processing:
    • FIFO → Queue<T>
    • LIFO → Stack<T>
    • Sorted → SortedSet<T>

Key Recommendations:

  • Start with List<T> or Dictionary<TKey,TValue> for most scenarios
  • Use specialized collections only when their unique features are needed
  • Always consider your most frequent operations (searching, inserting, accessing)
  • Prefer generic collections over legacy non-generic versions

Remember to profile performance with realistic data before optimizing. The right collection choice can significantly impact your application's efficiency and maintainability.