Arrays and Collections in C#
Learn arrays, List<T>, Dictionary<K,V>, HashSet<T>, Queue, Stack, and IEnumerable in C#.
Arrays
Arrays are fixed-size, zero-indexed, and the fastest collection for random access.
// Declaration and initialization
int[] numbers = new int[5]; // [0, 0, 0, 0, 0]
int[] primes = new int[] { 2, 3, 5, 7, 11 };
int[] odds = { 1, 3, 5, 7, 9 }; // implicit new
// Access and mutation
primes[0] = 2;
Console.WriteLine(primes[^1]); // 11 (last element)
Console.WriteLine(primes.Length); // 5
// Slicing with Range
int[] slice = primes[1..4]; // { 3, 5, 7 }
// 2D arrays
int[,] matrix = new int[3, 3];
matrix[0, 0] = 1;
int rows = matrix.GetLength(0); // 3
int cols = matrix.GetLength(1); // 3
// Jagged arrays (array of arrays — rows can differ in length)
int[][] jagged = new int[3][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
jagged[2] = new int[] { 6 };
// Sorting and searching
int[] data = { 5, 3, 8, 1, 9, 2 };
Array.Sort(data); // in-place sort
int idx = Array.BinarySearch(data, 8); // requires sorted array
Array.Reverse(data); // in-place reverse
// Copy
int[] copy = new int[data.Length];
Array.Copy(data, copy, data.Length);
int[] copy2 = (int[])data.Clone();
List<T>
List<T> is the most commonly used collection — a dynamically resized array.
using System.Collections.Generic;
var names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.AddRange(new[] { "Carol", "Dave" });
// Index access
Console.WriteLine(names[0]); // Alice
Console.WriteLine(names.Count); // 4
// Insert and Remove
names.Insert(1, "Ana"); // insert at index 1
names.Remove("Bob"); // remove first occurrence
names.RemoveAt(0); // remove by index
names.RemoveAll(n => n.StartsWith("A")); // remove all matching
// Search
bool has = names.Contains("Carol");
int idx = names.IndexOf("Carol");
string? found = names.Find(n => n.Length > 4); // first match
// Sort
names.Sort();
names.Sort((a, b) => a.Length.CompareTo(b.Length)); // by length
// Convert to array
string[] arr = names.ToArray();
// Capacity management (pre-allocate if count is known)
var scores = new List<int>(capacity: 1000);
Dictionary<TKey, TValue>
A hash map with O(1) average lookup, insert, and delete.
var scores = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 82,
["Carol"] = 91
};
// Add and update
scores["Dave"] = 88;
scores["Alice"] = 97; // update existing
// Safe access
if (scores.TryGetValue("Alice", out int aliceScore))
Console.WriteLine($"Alice: {aliceScore}");
// ContainsKey
bool hasEve = scores.ContainsKey("Eve"); // false
// Iteration
foreach (var (name, score) in scores)
Console.WriteLine($"{name}: {score}");
// Keys and Values collections
foreach (string key in scores.Keys)
Console.WriteLine(key);
// GetOrAdd pattern
scores.TryAdd("Eve", 79); // only adds if key doesn't exist
// Default value if missing
int fayeScore = scores.GetValueOrDefault("Faye", 0); // 0
// Remove
scores.Remove("Bob");
// Dictionary with complex keys
var lookup = new Dictionary<(int, int), string>();
lookup[(0, 0)] = "Origin";
HashSet<T>
A set that stores unique values with O(1) lookup.
var set = new HashSet<string> { "apple", "banana", "cherry" };
set.Add("apple"); // returns false — already present
set.Add("date"); // returns true
set.Remove("banana");
bool has = set.Contains("cherry"); // O(1) — much faster than List.Contains
// Set operations
var a = new HashSet<int> { 1, 2, 3, 4 };
var b = new HashSet<int> { 3, 4, 5, 6 };
a.UnionWith(b); // a = {1,2,3,4,5,6} — modifies a
a.IntersectWith(b); // a = {3,4} — modifies a
a.ExceptWith(b); // a = {1,2} — modifies a
bool isSubset = a.IsSubsetOf(b);
// Non-mutating (LINQ)
var union = a.Union(b);
var intersect = a.Intersect(b);
var except = a.Except(b);
Queue<T> and Stack<T>
// Queue — FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("Task A");
queue.Enqueue("Task B");
queue.Enqueue("Task C");
Console.WriteLine(queue.Peek()); // "Task A" — look without removing
string next = queue.Dequeue(); // "Task A" — removes it
Console.WriteLine(queue.Count); // 2
// Stack — LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine(stack.Peek()); // 3
int top = stack.Pop(); // 3
Console.WriteLine(string.Join(", ", stack)); // 2, 1
IEnumerable<T> and Lazy Sequences
IEnumerable<T> is the base interface for all sequences. LINQ operates on it.
// Any enumerable works with foreach
IEnumerable<int> GetEvens(int max)
{
for (int i = 0; i <= max; i += 2)
yield return i; // lazy — computes one at a time
}
foreach (int n in GetEvens(10))
Console.Write(n + " "); // 0 2 4 6 8 10
// LINQ on any IEnumerable
var result = GetEvens(100)
.Where(n => n % 6 == 0)
.Take(5)
.ToList();
// [0, 6, 12, 18, 24]
Specialized Collections
// SortedDictionary — keys in sorted order, O(log N) operations
var sorted = new SortedDictionary<string, int>
{
["Zebra"] = 1,
["Apple"] = 2,
["Mango"] = 3
};
// Iterates in key order: Apple, Mango, Zebra
// LinkedList — O(1) insert/remove at any position with a node reference
var linked = new LinkedList<int>();
linked.AddLast(1);
linked.AddLast(2);
var node = linked.AddLast(3);
linked.AddBefore(node, 99); // 1 → 2 → 99 → 3
// PriorityQueue (NET 6+)
var pq = new PriorityQueue<string, int>();
pq.Enqueue("Low priority", 3);
pq.Enqueue("High priority", 1);
pq.Enqueue("Mid priority", 2);
while (pq.TryDequeue(out string item, out int priority))
Console.WriteLine($"{priority}: {item}");
// 1: High priority
// 2: Mid priority
// 3: Low priority
Collection Initialization and LINQ Shortcuts
// Collection expressions (C# 12)
int[] arr = [1, 2, 3];
List<int> l = [1, 2, 3];
// Spread operator in collection expression
int[] more = [..arr, 4, 5]; // [1, 2, 3, 4, 5]
// Read-only wrappers
IReadOnlyList<string> readOnly = names.AsReadOnly();
IReadOnlyDictionary<string, int> readOnlyDict = scores.AsReadOnly();
Performance Comparison
| Collection | Index | Search | Insert Head | Insert Tail | Memory |
|---|---|---|---|---|---|
T[] | O(1) | O(N) | O(N) | — (fixed) | Compact |
List<T> | O(1) | O(N) | O(N) | O(1) amortized | Compact |
LinkedList<T> | O(N) | O(N) | O(1) | O(1) | Per-node overhead |
Dictionary<K,V> | — | O(1) avg | O(1) avg | O(1) avg | Hash buckets |
HashSet<T> | — | O(1) avg | O(1) avg | — | Hash buckets |
SortedDictionary<K,V> | — | O(log N) | O(log N) | O(log N) | Tree nodes |
Frequently Asked Questions
When should I use an array vs a List<T>?
Use arrays when the size is fixed and known upfront, or when you need maximum performance for indexed access (e.g., image pixel buffers). Use List<T> when you need to add or remove elements dynamically.
What is the difference between IEnumerable<T> and IList<T>?
IEnumerable<T> is a forward-only sequence — you can iterate it once. IList<T> adds indexed access, Count, Add, and Remove. Return IEnumerable<T> from APIs when callers only need to iterate; return IList<T> or IReadOnlyList<T> when they need random access.
Is Dictionary<K,V> thread-safe?
No. For concurrent reads and writes use ConcurrentDictionary<K,V> from System.Collections.Concurrent. For read-heavy workloads with infrequent writes, consider ImmutableDictionary<K,V>.