Collections
Definition
Collections in C# are data structures for storing, organizing, and managing groups of related objects. The .NET Base Class Library provides collections in the System.Collections.Generic namespace, offering type-safe, generic versions of classic data structures.
using System.Collections.Generic;
List<int> numbers = [1, 2, 3, 4, 5]; // Collection expression (C# 12)
Dictionary<string, int> ages = new() { ["Alice"] = 30, ["Bob"] = 25 };
HashSet<int> unique = [1, 2, 3];
Core Concepts
Arrays
Arrays are fixed-size, zero-indexed, and stored in contiguous memory. They are the most fundamental collection type in C#.
// Declaration and initialization
int[] nums = new int[5]; // [0, 0, 0, 0, 0]
int[] primes = [2, 3, 5, 7, 11]; // Collection expression (C# 12)
string[] names = ["Alice", "Bob"];
// Multi-dimensional arrays
int[,] grid = new int[3, 4]; // 3 rows, 4 columns
int[,] matrix = { { 1, 2 }, { 3, 4 } };
// Jagged arrays (array of arrays)
int[][] jagged = new int[3][];
jagged[0] = [1, 2];
jagged[1] = [3, 4, 5];
jagged[2] = [6];
Use arrays when the size is known at creation and does not change. They offer the best performance due to contiguous memory layout.
List<T>
A dynamic array that automatically resizes. The most commonly used collection in C#.
List<int> list = [3, 1, 4, 1, 5];
list.Add(9); // Append to end
list.AddRange([2, 6, 5]); // Add multiple
list.Insert(0, 99); // Insert at index
list.Remove(1); // Remove first occurrence of 1
list.RemoveAt(0); // Remove by index
list.Sort(); // Sort in place
bool has = list.Contains(4); // O(n) search
int? found = list.Find(x => x > 3); // First match
List<int> all = list.FindAll(x => x > 2); // All matches
int count = list.Count; // Actual number of elements
int cap = list.Capacity; // Allocated internal array size
Count returns the number of elements. Capacity returns the size of the internal array. Capacity >= Count always holds.
Dictionary<TKey, TValue>
Stores key-value pairs with O(1) average lookup by key. Keys must be unique.
var scores = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 87
};
// Add and retrieve
scores["Charlie"] = 72;
int aliceScore = scores["Alice"]; // 95 (throws if key missing)
// Safe retrieval
if (scores.TryGetValue("Bob", out int bobScore))
{
Console.WriteLine(bobScore); // 87
}
// Check existence
bool hasKey = scores.ContainsKey("Alice");
bool hasVal = scores.ContainsValue(87); // O(n)
// Iteration
foreach (KeyValuePair<string, int> kvp in scores)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
HashSet<T>
Stores unique elements with O(1) average lookup. Supports mathematical set operations.
HashSet<int> setA = [1, 2, 3, 4, 5];
HashSet<int> setB = [4, 5, 6, 7, 8];
setA.Add(6); // Add (ignored if exists)
setA.UnionWith(setB); // A = A U B
setA.IntersectWith(setB); // A = A ^ B
setA.ExceptWith(setB); // A = A - B
bool subset = setA.IsSubsetOf(setB); // Is A a subset of B?
bool overlap = setA.Overlaps(setB); // Any common elements?
Queue<T>
FIFO (First-In, First-Out) data structure.
var queue = new Queue<string>();
queue.Enqueue("first"); // Add to back
queue.Enqueue("second");
string front = queue.Peek(); // View front without removing: "first"
string dequeued = queue.Dequeue(); // Remove and return front: "first"
// Safe dequeue
if (queue.TryDequeue(out string? item))
{
Console.WriteLine(item); // "second"
}
Stack<T>
LIFO (Last-In, First-Out) data structure.
var stack = new Stack<int>();
stack.Push(1); // Add to top
stack.Push(2);
stack.Push(3);
int top = stack.Peek(); // View top without removing: 3
int popped = stack.Pop(); // Remove and return top: 3
// Safe pop
if (stack.TryPop(out int item))
{
Console.WriteLine(item); // 2
}
Non-Generic Collections (Legacy)
The System.Collections namespace provides non-generic collections that store elements as object. Prefer the generic versions in System.Collections.Generic for type safety and to avoid boxing/unboxing overhead.
Queue and Stack (Non-Generic)
using System.Collections; // non-generic namespace
// Non-generic Queue — stores object, requires casting
Queue queue = new Queue();
queue.Enqueue(42);
int val = (int)queue.Dequeue(); // explicit cast needed
// Non-generic Stack — stores object, requires casting
Stack stack = new Stack();
stack.Push(99);
int top = (int)stack.Pop(); // explicit cast needed
Always prefer Queue<T> and Stack<T> over their non-generic counterparts. The non-generic versions store object, causing boxing for value types and requiring runtime casts that can fail.