Skip to content

C# Arrays and Collections — Arrays, List, Dictionary, HashSet, and Collection Initializers

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C# Arrays and Collections. We cover key concepts, practical examples, and best practices to help you master this topic.

C# collections are data structures for storing and organizing multiple values, with arrays for fixed-size storage, List for dynamic lists, Dictionary<TKey,TValue> for key-value lookups, and HashSet for unique element sets.

What You'll Learn

You will master collections in C#: arrays for fixed-size contiguous storage, List<T> for dynamic resizable collections, Dictionary<TKey, TValue> for fast key-based lookup, HashSet<T> for unique element sets, collection initializers, and how these collections integrate with .NET LINQ and the type system.

Why It Matters

Choosing the right collection type directly affects application performance and code clarity. Using a List when you need Dictionary-level lookups causes O(n) performance instead of O(1). Using an array when you need dynamic sizing requires manual resizing. Understanding collection characteristics is essential for writing efficient C# code.

Real-World Use

Web applications use Dictionary for HTTP header collections and route parameters. E-commerce systems use HashSet for tracking unique product IDs in shopping carts. Data processing uses List for dynamic result sets. Caching systems use ConcurrentDictionary for thread-safe access. Game engines use arrays for vertex data due to contiguous memory layout.

Learning Path

graph LR
    A["17: Strings"] --> B["18: Arrays & Collections"]
    B --> C["19: Generics"]
    C --> D["20: Exception Handling"]
    D --> E["21: LINQ"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Arrays

Fixed-size, contiguous memory storage:

// Declaration and initialization
int[] numbers = new int[5];              // All zeros
int[] primes = new int[] { 2, 3, 5, 7, 11 };
int[] squares = { 1, 4, 9, 16, 25 };    // Shorthand

// Accessing elements
Console.WriteLine(primes[0]);    // 2
primes[0] = 42;                 // Modify

// Length (fixed)
Console.WriteLine(primes.Length);  // 5

// Iteration
for (int i = 0; i < primes.Length; i++)
    Console.Write($"{primes[i]} ");

Console.WriteLine();
foreach (int p in primes)
    Console.Write($"{p} ");

// Multidimensional arrays
int[,] matrix = new int[3, 3];
matrix[0, 0] = 1;
int[][] jagged = new int[3][];  // Array of arrays
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };

// Array methods
int[] copy = new int[5];
Array.Copy(primes, copy, 3);     // Copy first 3 elements
Array.Sort(primes);              // Sort in place
int index = Array.IndexOf(primes, 7);  // Find index
Array.Reverse(primes);           // Reverse in place
Array.Fill(numbers, -1);         // Fill all elements

List

Dynamic, resizable collection:

// Creating lists
List<int> numbers = new List<int>();
var names = new List<string> { "Alice", "Bob", "Charlie" };
var capacity = new List<int>(100);  // Pre-allocate capacity

// Adding elements
numbers.Add(10);
numbers.AddRange(new[] { 20, 30, 40 });
numbers.Insert(1, 15);  // Insert at index

// Accessing
Console.WriteLine(numbers[0]);  // 10
Console.WriteLine(numbers.Count);  // 5

// Removing
numbers.Remove(15);         // Remove by value
numbers.RemoveAt(0);        // Remove by index
numbers.RemoveAll(n => n > 30);  // Remove by condition

// Searching
bool exists = numbers.Contains(20);
int index = numbers.IndexOf(20);

// Sorting and conversion
numbers.Sort();
int[] array = numbers.ToArray();

// List-specific operations
var list = new List<int> { 1, 2, 3, 4, 5 };
list.Reverse();
Console.WriteLine(string.Join(", ", list));  // 5, 4, 3, 2, 1

var slice = list.GetRange(1, 3);  // Index 1, count 3
Console.WriteLine(string.Join(", ", slice));  // 4, 3, 2

Dictionary<TKey, TValue>

Fast key-value lookups:

// Creating dictionaries
var scores = new Dictionary<string, int>();
var config = new Dictionary<string, string>
{
    ["ServerUrl"] = "https://api.example.com",
    ["Timeout"] = "30",
    ["RetryCount"] = "3"
};

// Adding entries
scores.Add("Alice", 95);
scores["Bob"] = 87;  // Same as Add if key doesn't exist
scores["Charlie"] = 92;

// Accessing (safe)
if (scores.TryGetValue("Alice", out int aliceScore))
{
    Console.WriteLine($"Alice: {aliceScore}");
}

// Checking existence
bool hasKey = scores.ContainsKey("David");
bool hasValue = scores.ContainsValue(95);  // Slow for large dictionaries

// Iteration
foreach (KeyValuePair<string, int> kvp in scores)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

foreach (var key in scores.Keys)
    Console.WriteLine(key);

foreach (var value in scores.Values)
    Console.WriteLine(value);

// Removing
scores.Remove("Charlie");

// Default value pattern
int davidScore = scores.GetValueOrDefault("David", 0);
Console.WriteLine(davidScore);  // 0

HashSet

Unique elements with fast set operations:

var unique = new HashSet<int> { 1, 2, 3, 3, 2, 1 };
Console.WriteLine(string.Join(", ", unique));  // 1, 2, 3

// Adding
unique.Add(4);
bool added = unique.Add(4);  // False (already exists)

// Set operations
var setA = new HashSet<int> { 1, 2, 3, 4 };
var setB = new HashSet<int> { 3, 4, 5, 6 };

setA.IntersectWith(setB);     // { 3, 4 }
setA.UnionWith(setB);         // { 1, 2, 3, 4, 5, 6 }
setA.ExceptWith(setB);        // { 1, 2 }
setA.SymmetricExceptWith(setB); // Elements in one but not both

// Checking
bool contains = setA.Contains(3);
bool isSubset = setA.IsSubsetOf(setB);
bool overlaps = setA.Overlaps(setB);

Collection Initializers

// List
List<int> list = new List<int> { 1, 2, 3 };

// Dictionary
var dict = new Dictionary<int, string>
{
    { 1, "one" },
    { 2, "two" },
    { 3, "three" }
};

// Or with index initializers (C# 6+)
var dict2 = new Dictionary<int, string>
{
    [1] = "one",
    [2] = "two",
};

// HashSet
var set = new HashSet<string> { "apple", "banana", "cherry" };

// Custom collection initializer
public class Team
{
    public List<string> Members { get; } = new();
    public void Add(string member) => Members.Add(member);
}

var team = new Team { "Alice", "Bob", "Charlie" };

Collection Performance

Collection Access Search Add Remove Memory
Array O(1) O(n) O(1)* O(n) Low
List O(1) O(n) O(1)** O(n) Low
Dictionary - O(1) O(1) O(1) Medium
HashSet - O(1) O(1) O(1) Medium
Stack - O(n) O(1) O(1) Low
Queue - O(n) O(1) O(1) Low

*Array size is fixed; adding beyond capacity requires new allocation **Amortized O(1); O(n) when internal array needs resizing

Choosing the Right Collection

// Need sequential access by index? Use array or List<T>
var users = new List<User>(LoadUsers());

// Need fast key lookup? Use Dictionary
var userById = new Dictionary<int, User>();
foreach (var user in users)
    userById[user.Id] = user;

// Need unique items with fast lookup? Use HashSet
var processedIds = new HashSet<int>();

// Need LIFO order? Use Stack<T>
var navigation = new Stack<string>();
navigation.Push("Page1");
navigation.Push("Page2");
string back = navigation.Pop();  // Page2

// Need FIFO order? Use Queue<T>
var queue = new Queue<Task>();
queue.Enqueue(new Task("Task1"));
queue.Enqueue(new Task("Task2"));
Task next = queue.Dequeue();  // Task1

// Need sorted key-value pairs? Use SortedDictionary or SortedList
var sorted = new SortedDictionary<string, int>();

Common Mistakes

Mistake 1: Using List When Dictionary Is Appropriate

Linear search in a List is O(n). If you frequently look up by a key, use Dictionary for O(1) lookups.

Mistake 2: Not Pre-Allocating Capacity

new List<int>(100000) allocates the internal array once. Without capacity, the list resizes multiple times, causing unnecessary allocations and copies.

Mistake 3: Modifying Collections During Enumeration

Adding or removing items in a foreach loop throws InvalidOperationException. Collect changes in a separate list, then apply after iteration.

Mistake 4: Using Array of the Wrong Size

Arrays have fixed size. If you need dynamic sizing, use List. Resizing arrays manually with Array.Resize is inefficient.

Mistake 5: Ignoring Dictionary Key Equality

Dictionary keys use the default equality comparer. For custom types, override Equals and GetHashCode, or provide an IEqualityComparer<T>.

Mistake 6: Confusing Count and Capacity

List.Capacity is the internal array size. List.Count is the actual number of elements. Capacity >= Count.

Practice Questions

  1. When would you use a Dictionary<string, T> instead of a List?
  2. What is the difference between an array and a List?
  3. How does HashSet ensure element uniqueness?
  4. Why would you pre-allocate capacity in a List?
  5. Write code to count word frequencies in a string using Dictionary<string, int>.

Challenge

Implement a simple in-memory cache using Dictionary<string, (object Value, DateTime ExpiresAt)> that supports expiration. Add methods for Get, Set with TTL, and automatic cleanup of expired entries.

FAQ

What is the difference between `Count` and `Count()`?

Count is a property on List and most collections (O(1)). Count() is a LINQ extension method that may iterate the collection (O(n)). Use Count when it is available.

Can I use a custom type as a Dictionary key?

Yes. Custom types used as Dictionary keys must implement proper equality semantics. Override Equals and GetHashCode, or implement IEquatable.

What happens when a List exceeds its capacity?

The internal array is replaced with a new array of double the size. All elements are copied. This is O(n) and causes a GC spike for large lists.

Is it safe to iterate a collection from multiple threads?

No. Standard collections are not thread-safe. Use ConcurrentDictionary, ConcurrentQueue, ConcurrentBag, or lock around access for thread safety.

What is the difference between `IEnumerable` and `IQueryable`?

IEnumerable is for in-memory collections evaluated client-side. IQueryable is for remote data sources (databases) where the query is translated and executed server-side.

Mini Project

Create an in-memory data store with indexing:

public class InMemoryStore<T>
{
    private readonly Dictionary<int, T> _items = new();
    private readonly Dictionary<string, Dictionary<object, List<int>>> _indexes = new();
    private int _nextId = 1;

    public int Insert(T item)
    {
        int id = _nextId++;
        _items[id] = item;
        UpdateIndexes(id, item);
        return id;
    }

    public T? Get(int id) => _items.GetValueOrDefault(id);

    public List<T> Query(string fieldName, object value)
    {
        if (!_indexes.TryGetValue(fieldName, out var fieldIndex))
            return new List<T>();

        if (!fieldIndex.TryGetValue(value, out var ids))
            return new List<T>();

        return ids.Select(id => _items[id]).ToList();
    }

    private void UpdateIndexes(int id, T item)
    {
        foreach (var prop in typeof(T).GetProperties())
        {
            var value = prop.GetValue(item);
            if (value == null) continue;

            if (!_indexes.ContainsKey(prop.Name))
                _indexes[prop.Name] = new Dictionary<object, List<int>>();

            var fieldIndex = _indexes[prop.Name];
            if (!fieldIndex.ContainsKey(value))
                fieldIndex[value] = new List<int>();

            fieldIndex[value].Add(id);
        }
    }
}

var store = new InMemoryStore<Dictionary<string, object>>();

var alice = new Dictionary<string, object> { { "Name", "Alice" }, { "City", "NYC" }, { "Age", 30 } };
var bob = new Dictionary<string, object> { { "Name", "Bob" }, { "City", "LA" }, { "Age", 25 } };
var charlie = new Dictionary<string, object> { { "Name", "Charlie" }, { "City", "NYC" }, { "Age", 35 } };

store.Insert(alice);
store.Insert(bob);
store.Insert(charlie);

var nycUsers = store.Query("City", "NYC");
Console.WriteLine("Users in NYC:");
foreach (var user in nycUsers)
    Console.WriteLine($"  {user["Name"]}, Age: {user["Age"]}");

Expected output:

Users in NYC:
  Alice, Age: 30
  Charlie, Age: 35

What's Next

You have mastered arrays and collections in C#. The next lesson covers generics: type parameters, constraints, covariance, and contravariance.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro