Home C# / .NET C# Arrays and Collections Explained — From Zero to Confident

C# Arrays and Collections Explained — From Zero to Confident

In Plain English 🔥
Imagine you're organising a birthday party. A single variable is like holding one balloon in your hand — great for one thing. An array is like a balloon rack with fixed, numbered slots — you decide upfront you need exactly 10 balloons, and each slot has a number starting at zero. A List is like a stretchy gift bag — you can keep stuffing more presents in without deciding the size in advance. A Dictionary is like a labelled coat-check counter — instead of a number, each item has a unique name tag (like 'Alice's coat') so you can grab exactly the right one instantly.
⚡ Quick Answer
Imagine you're organising a birthday party. A single variable is like holding one balloon in your hand — great for one thing. An array is like a balloon rack with fixed, numbered slots — you decide upfront you need exactly 10 balloons, and each slot has a number starting at zero. A List is like a stretchy gift bag — you can keep stuffing more presents in without deciding the size in advance. A Dictionary is like a labelled coat-check counter — instead of a number, each item has a unique name tag (like 'Alice's coat') so you can grab exactly the right one instantly.

Every real app — from a shopping cart to a leaderboard to a contact book — needs to store more than one piece of data at a time. The moment you move beyond a single variable, you need a structure that can hold a group of values and let you work with them together. That's exactly what arrays and collections do, and they are arguably the most-used tools in any C# developer's daily work. Knowing them well separates a developer who struggles with basic data wrangling from one who writes clean, confident code.

Before collections existed, developers had to manually juggle groups of related data — declaring ten separate variables for ten students, copying data into new arrays whenever the size changed, writing fragile code that broke if you added one extra item. Arrays gave us numbered slots in memory. The Collections namespace in .NET went further, giving us dynamic, purpose-built containers like List, Dictionary, Queue, and Stack that handle growth, lookup, and ordering automatically.

By the end of this article you'll be able to declare and use arrays, create and manipulate List and Dictionary, iterate over any collection with a foreach loop, choose the right container for a given problem, and avoid the three classic mistakes that trip up almost every beginner. Let's build this up from scratch.

C# Arrays — Fixed-Size Storage With Numbered Slots

An array is the simplest way to store multiple values of the same type. Think of it as a row of labelled lockers in a school corridor. You decide how many lockers you need when you build the corridor — that number never changes. Each locker has a number starting at zero (not one — this trips people up constantly). Locker zero is the first one.

You declare an array by writing the type, square brackets, a name, and then using new to create it with a fixed size. Once created, every slot is automatically filled with the default value for that type — zero for numbers, null for strings.

Arrays are the right choice when you know exactly how many items you'll have and that number won't change — days of the week, months of the year, RGB colour channels. They are blazing fast because the computer lays them out in a straight line in memory. But if the size might change — use a List instead, which we'll cover next.

The .Length property tells you how many slots exist. Accessing a slot that doesn't exist (like index 10 in a 5-slot array) throws an IndexOutOfRangeException — one of the most common crashes beginners see.

ArrayBasics.cs · CSHARP
123456789101112131415161718192021222324252627282930313233343536373839404142434445
using System;

class ArrayBasics
{
    static void Main()
    {
        // --- 1. Declaring and creating an array ---
        // We know there are exactly 5 weekdays, so an array is perfect here.
        string[] weekdays = new string[5];

        // Assign a value to each numbered slot (index starts at 0, not 1)
        weekdays[0] = "Monday";
        weekdays[1] = "Tuesday";
        weekdays[2] = "Wednesday";
        weekdays[3] = "Thursday";
        weekdays[4] = "Friday";

        // --- 2. Shorthand initialiser — declare and fill in one line ---
        int[] highScores = { 9800, 7650, 6200, 5100, 3400 };

        // --- 3. Reading a single value by its index ---
        Console.WriteLine("First weekday: " + weekdays[0]);   // Monday
        Console.WriteLine("Top score: " + highScores[0]);     // 9800
        Console.WriteLine("Third score: " + highScores[2]);   // 6200 (index 2 = 3rd item)

        // --- 4. Looping over every item with foreach ---
        Console.WriteLine("\n--- All Weekdays ---");
        foreach (string day in weekdays)
        {
            // 'day' is a temporary variable that holds the current item each loop
            Console.WriteLine(day);
        }

        // --- 5. Looping with index using a for loop (useful when you need the position) ---
        Console.WriteLine("\n--- High Score Leaderboard ---");
        for (int position = 0; position < highScores.Length; position++)
        {
            // position + 1 because humans count from 1, not 0
            Console.WriteLine($"#{position + 1}: {highScores[position]} points");
        }

        // --- 6. Array.Length tells you how many slots exist ---
        Console.WriteLine($"\nTotal scores stored: {highScores.Length}");
    }
}
▶ Output
First weekday: Monday
Top score: 9800
Third score: 6200

--- All Weekdays ---
Monday
Tuesday
Wednesday
Thursday
Friday

--- High Score Leaderboard ---
#1: 9800 points
#2: 7650 points
#3: 6200 points
#4: 5100 points
#5: 3400 points

Total scores stored: 5
⚠️
Watch Out: Indexes Start at Zero, AlwaysThe first element is always at index 0, not 1. A 5-element array has valid indexes 0 through 4. Trying to access index 5 throws an IndexOutOfRangeException at runtime — the compiler won't catch this for you. Always use `array.Length - 1` if you need the last item, or just use `array[^1]` in modern C# (version 8+).

List — The Dynamic Array That Grows With You

The moment you need a collection whose size isn't fixed — a shopping cart that can have any number of items, a list of players that join and leave — an array becomes a headache. You'd have to create a new, bigger array and copy everything over every time you wanted to add an item. That's exactly the problem List solves.

List is a generic collection. The T is a placeholder for the type you want to store — List stores strings, List stores integers. Think of it as an array with superpowers: it resizes itself automatically, gives you .Add(), .Remove(), .Contains(), .Count, and dozens of other useful methods out of the box.

Under the hood, a List actually is backed by an array — it just handles all the resizing work for you. When it runs out of room, it silently creates a new internal array twice the size and copies everything across. You never have to think about this, but it's good to know so you understand why arrays are marginally faster for fixed data.

Use List as your default collection whenever the size might change or you don't know it upfront. It's the workhorse of C# development — you'll use it in virtually every project you write.

ListBasics.cs · CSHARP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
using System;
using System.Collections.Generic; // Required to use List<T>

class ListBasics
{
    static void Main()
    {
        // --- 1. Creating an empty List and adding items dynamically ---
        List<string> shoppingCart = new List<string>();

        shoppingCart.Add("Apples");     // Adds to the end of the list
        shoppingCart.Add("Bread");
        shoppingCart.Add("Milk");
        shoppingCart.Add("Cheese");

        Console.WriteLine($"Items in cart: {shoppingCart.Count}"); // Count, not Length

        // --- 2. Initialiser syntax — create with starting values ---
        List<int> playerScores = new List<int> { 450, 820, 310, 990, 670 };

        // --- 3. Access by index — works just like an array ---
        Console.WriteLine("First cart item: " + shoppingCart[0]); // Apples

        // --- 4. Check if an item exists before acting on it ---
        if (shoppingCart.Contains("Milk"))
        {
            Console.WriteLine("Milk is in the cart.");
        }

        // --- 5. Remove a specific item by value ---
        shoppingCart.Remove("Bread"); // Finds and removes the first matching item
        Console.WriteLine($"After removing Bread: {shoppingCart.Count} items");

        // --- 6. Insert at a specific position (index 1 = second slot) ---
        shoppingCart.Insert(1, "Butter");

        // --- 7. Iterate with foreach — cleanest way to read all items ---
        Console.WriteLine("\n--- Final Shopping Cart ---");
        foreach (string item in shoppingCart)
        {
            Console.WriteLine("  - " + item);
        }

        // --- 8. Sort the list in place ---
        playerScores.Sort();
        Console.WriteLine("\n--- Sorted Scores (low to high) ---");
        foreach (int score in playerScores)
        {
            Console.Write(score + " ");
        }
        Console.WriteLine();

        // --- 9. Convert a List back to an array if ever needed ---
        string[] cartArray = shoppingCart.ToArray();
        Console.WriteLine($"\nArray version has {cartArray.Length} elements.");
    }
}
▶ Output
Items in cart: 4
First cart item: Apples
Milk is in the cart.
After removing Bread: 3 items

--- Final Shopping Cart ---
- Apples
- Butter
- Milk
- Cheese

--- Sorted Scores (low to high) ---
310 450 670 820 990

Array version has 4 elements.
⚠️
Pro Tip: Use .Count, Not .Length on a ListArrays use `.Length`. Lists use `.Count`. They do the same thing — tell you how many items are stored — but using the wrong one is a compiler error that catches out almost every beginner on their first day with collections. The rule is simple: if it ends in `[]` it's an array, use `.Length`. If it's a `List`, use `.Count`.

Dictionary — Lightning-Fast Lookup by Name

Sometimes a numbered index isn't the right way to identify data. You don't want player stats at index 3 — you want them by player name. You don't want a country's capital at index 47 — you want it by the country's name. That's what Dictionary is for.

A dictionary stores key-value pairs. Every item has a unique key (like a name, an ID, a code) and a value (the data attached to that key). Think of a real physical dictionary: you look up a word (the key) and get the definition (the value) instantly — you don't have to read every page to find it.

Lookup in a dictionary is extremely fast — essentially instant regardless of how many items are stored — because .NET uses a technique called hashing internally. Compare this to searching through a List where, in the worst case, you have to check every single item.

Keys must be unique. You can't have two entries with the key "Alice" — the second add will throw an exception. Values can be duplicated — two players can have the same score. Use ContainsKey() before adding if you're not sure whether the key exists yet.

Use a Dictionary whenever you need to look things up by a meaningful name or ID rather than a position number.

DictionaryBasics.cs · CSHARP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
using System;
using System.Collections.Generic;

class DictionaryBasics
{
    static void Main()
    {
        // --- 1. Creating a Dictionary: key is country name, value is its capital ---
        Dictionary<string, string> capitalCities = new Dictionary<string, string>();

        // Add key-value pairs using Add()
        capitalCities.Add("France", "Paris");
        capitalCities.Add("Japan", "Tokyo");
        capitalCities.Add("Brazil", "Brasília");
        capitalCities.Add("Canada", "Ottawa");

        // --- 2. Initialiser syntax — cleaner when you know values upfront ---
        Dictionary<string, int> playerHighScores = new Dictionary<string, int>
        {
            { "Alice",   9800 },
            { "Bob",     7200 },
            { "Charlie", 8450 }
        };

        // --- 3. Look up a value by its key ---
        string japanCapital = capitalCities["Japan"]; // Returns "Tokyo"
        Console.WriteLine("Capital of Japan: " + japanCapital);

        // --- 4. Safe lookup with TryGetValue — prevents crashes on missing keys ---
        string countryToFind = "Germany";
        if (capitalCities.TryGetValue(countryToFind, out string capital))
        {
            Console.WriteLine($"Capital of {countryToFind}: {capital}");
        }
        else
        {
            // This branch runs because Germany is not in our dictionary
            Console.WriteLine($"{countryToFind} is not in the dictionary.");
        }

        // --- 5. Update a value — just assign to the key directly ---
        playerHighScores["Bob"] = 8100; // Bob beat his old score
        Console.WriteLine($"Bob's updated score: {playerHighScores["Bob"]}");

        // --- 6. Check if a key exists before adding to avoid duplicate key exception ---
        string newPlayer = "Alice";
        if (!playerHighScores.ContainsKey(newPlayer))
        {
            playerHighScores.Add(newPlayer, 5000);
        }
        else
        {
            Console.WriteLine($"{newPlayer} already has a score on record.");
        }

        // --- 7. Loop over all key-value pairs using KeyValuePair ---
        Console.WriteLine("\n--- All Player High Scores ---");
        foreach (KeyValuePair<string, int> entry in playerHighScores)
        {
            // entry.Key is the player name, entry.Value is their score
            Console.WriteLine($"  {entry.Key}: {entry.Value} points");
        }

        // --- 8. Loop over just the keys or just the values ---
        Console.WriteLine("\n--- Countries in our dictionary ---");
        foreach (string country in capitalCities.Keys)
        {
            Console.WriteLine("  " + country);
        }

        Console.WriteLine($"\nTotal entries: {capitalCities.Count}");
    }
}
▶ Output
Capital of Japan: Tokyo
Germany is not in the dictionary.
Bob's updated score: 8100
Alice already has a score on record.

--- All Player High Scores ---
Alice: 9800 points
Bob: 8100 points
Charlie: 8450 points

--- Countries in our dictionary ---
France
Japan
Brazil
Canada

Total entries: 4
⚠️
Watch Out: Dictionaries Don't Guarantee OrderA Dictionary does not store items in the order you added them. If you need items in insertion order, use a List or in .NET 5+ you can rely on Dictionary maintaining insertion order as an implementation detail — but never depend on it as a guarantee. If ordered key-value pairs matter, use `SortedDictionary` instead.

Choosing the Right Collection — Arrays vs List vs Dictionary

Now you know all three — the question is when to reach for which one. The wrong choice won't break your code immediately, but it'll make it slower, harder to read, or fragile over time.

Here's the practical mental model: start by asking yourself two questions. First — do I know the exact number of items upfront and will it never change? If yes, an array is perfect. Second — do I need to look things up by a meaningful label (name, ID, code) rather than a position? If yes, a Dictionary is your tool. For everything else — a flexible, ordered list of items you'll add to, remove from, or iterate — use a List.

In real projects, List is by far the most common collection you'll use. Dictionary is your second most common. Raw arrays appear most often when working with performance-sensitive code, interfacing with older APIs, or representing fixed data like a grid or a colour palette.

There are other collections worth knowing about — HashSet stores unique values with no duplicates, Queue is a first-in-first-out line (like a printer queue), and Stack is last-in-first-out (like a stack of plates). But master arrays, List, and Dictionary first. They cover 90% of real scenarios.

ChoosingCollections.cs · CSHARP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
using System;
using System.Collections.Generic;

class ChoosingCollections
{
    static void Main()
    {
        // SCENARIO 1: Fixed data that never changes → use an ARRAY
        // The number of months in a year is always 12.
        string[] monthNames = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
        Console.WriteLine("Month 3: " + monthNames[2]); // Index 2 = March

        // SCENARIO 2: Growing or shrinking list of items → use a LIST
        // A to-do list can have any number of tasks.
        List<string> todoList = new List<string>();
        todoList.Add("Buy groceries");
        todoList.Add("Book dentist");
        todoList.Add("Reply to emails");
        todoList.Remove("Book dentist"); // Done!

        Console.WriteLine("\n--- To-Do List ---");
        foreach (string task in todoList)
        {
            Console.WriteLine("  [ ] " + task);
        }

        // SCENARIO 3: Look up by a meaningful key → use a DICTIONARY
        // Product catalogue: look up price by product code instantly.
        Dictionary<string, decimal> productCatalogue = new Dictionary<string, decimal>
        {
            { "SKU-001", 29.99m },
            { "SKU-002", 14.50m },
            { "SKU-003", 59.00m }
        };

        string scannedCode = "SKU-002";
        if (productCatalogue.TryGetValue(scannedCode, out decimal price))
        {
            Console.WriteLine($"\nProduct {scannedCode} costs £{price:F2}");
        }

        // BONUS: HashSet — when you only want UNIQUE values (no duplicates allowed)
        HashSet<string> uniqueTags = new HashSet<string>();
        uniqueTags.Add("csharp");
        uniqueTags.Add("dotnet");
        uniqueTags.Add("csharp"); // Duplicate — silently ignored, no error thrown
        Console.WriteLine($"\nUnique tags count: {uniqueTags.Count}"); // 2, not 3
    }
}
▶ Output
Month 3: Mar

--- To-Do List ---
[ ] Buy groceries
[ ] Reply to emails

Product SKU-002 costs £14.50

Unique tags count: 2
🔥
Interview Gold: The 'When Would You Use Each?' QuestionInterviewers love asking 'when would you choose an array over a List?' The answer they want: use an array when the size is fixed and performance is critical (e.g., pixel buffers, mathematical vectors). Use List when the size is dynamic. Use Dictionary when you need O(1) key-based lookup. Knowing the 'why' behind each choice signals that you think about performance and design, not just syntax.
Feature / AspectArray (string[])ListDictionary
SizeFixed at creation — cannot growDynamic — grows automaticallyDynamic — grows automatically
Access methodBy index: arr[0]By index: list[0]By key: dict["name"]
Add items after creationNot possible without new arraylist.Add(item) — easydict.Add(key, value)
Remove itemsNot possible — must create new arraylist.Remove(item) — easydict.Remove(key)
Check if item existsManual loop requiredlist.Contains(item) — O(n)dict.ContainsKey(k) — O(1) fast
Duplicate valuesAllowedAllowedKeys must be unique; values can duplicate
Preserves insertion orderYesYesNot guaranteed (implementation detail only)
Best used forFixed-size, performance-critical dataDefault go-to for any ordered listFast lookup by meaningful key
Property for item count.Length.Count.Count

🎯 Key Takeaways

  • Arrays are fixed-size and zero-indexed — slot 0 is first, slot (Length-1) is last. Use them for data that never changes in size.
  • List is your everyday workhorse — it resizes itself, supports Add/Remove/Contains, and uses .Count (not .Length) to tell you how many items it holds.
  • Dictionary gives you near-instant lookup by a meaningful key — use TryGetValue() instead of the indexer to avoid crashes on missing keys.
  • Never add to or remove from a List inside a foreach loop — it throws at runtime. Collect changes and apply them after the loop.

⚠ Common Mistakes to Avoid

  • Mistake 1: Off-by-one index errors — Accessing array[array.Length] instead of array[array.Length - 1] for the last element. Symptom: IndexOutOfRangeException at runtime. Fix: Remember arrays are zero-indexed, so a 5-element array has valid indexes 0–4. Use array[^1] in C# 8+ for the last element, or array[array.Length - 1] in older versions.
  • Mistake 2: Adding a duplicate key to a Dictionary — Calling dictionary.Add(existingKey, value) when that key already exists throws ArgumentException: An item with the same key has already been added. Fix: Always check first with if (!dict.ContainsKey(key)) before calling .Add(), or use the indexer dict[key] = value which safely overwrites an existing entry instead of throwing.
  • Mistake 3: Modifying a List while iterating over it with foreach — Adding or removing items inside a foreach loop throws InvalidOperationException: Collection was modified; enumeration operation may not execute. Fix: Either iterate over a copy (foreach (var item in myList.ToList())), or collect items to remove in a separate list and remove them after the loop finishes.

Interview Questions on This Topic

  • QWhat is the difference between an Array and a List in C#, and when would you choose one over the other?
  • QIf you need to look up a user's profile by their username thousands of times per second, which C# collection would you use and why?
  • QWhat happens if you call dictionary['missingKey'] on a Dictionary when that key doesn't exist — and what is the safer alternative?

Frequently Asked Questions

What is the difference between an array and a List in C#?

An array has a fixed size you set at creation and cannot change — it's fast and simple but inflexible. A List resizes itself automatically as you add or remove items and comes with helpful methods like Add(), Remove(), and Contains(). Use an array when the size is known and constant; use a List when it isn't.

How do I avoid a KeyNotFoundException when reading from a C# Dictionary?

Instead of accessing a value directly with dict[key] (which throws if the key is missing), use dict.TryGetValue(key, out var value). It returns true if the key was found and puts the value in the value variable, or returns false without throwing any exception if the key doesn't exist.

Can a C# List hold different types — like a mix of strings and numbers?

Not directly with a typed List. A List can only hold strings and a List can only hold integers — the compiler enforces this, which prevents bugs. If you genuinely need mixed types, you can use List, but this loses type safety and requires casting, so it's usually a sign you need a better data structure like a class or record instead.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousMethods and Parameters in C#Next →Strings in C#
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged