Skip to main content
C# beginner Lesson 10 of 25

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

CollectionIndexSearchInsert HeadInsert TailMemory
T[]O(1)O(N)O(N)— (fixed)Compact
List<T>O(1)O(N)O(N)O(1) amortizedCompact
LinkedList<T>O(N)O(N)O(1)O(1)Per-node overhead
Dictionary<K,V>O(1) avgO(1) avgO(1) avgHash buckets
HashSet<T>O(1) avgO(1) avgHash 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>.