C# Arrays and Collections Explained — From Zero to Confident
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
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.
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}"); } }
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
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.
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."); } }
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.
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.
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}"); } }
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
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.
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 } }
--- To-Do List ---
[ ] Buy groceries
[ ] Reply to emails
Product SKU-002 costs £14.50
Unique tags count: 2
| Feature / Aspect | Array (string[]) | List | Dictionary |
|---|---|---|---|
| Size | Fixed at creation — cannot grow | Dynamic — grows automatically | Dynamic — grows automatically |
| Access method | By index: arr[0] | By index: list[0] | By key: dict["name"] |
| Add items after creation | Not possible without new array | list.Add(item) — easy | dict.Add(key, value) |
| Remove items | Not possible — must create new array | list.Remove(item) — easy | dict.Remove(key) |
| Check if item exists | Manual loop required | list.Contains(item) — O(n) | dict.ContainsKey(k) — O(1) fast |
| Duplicate values | Allowed | Allowed | Keys must be unique; values can duplicate |
| Preserves insertion order | Yes | Yes | Not guaranteed (implementation detail only) |
| Best used for | Fixed-size, performance-critical data | Default go-to for any ordered list | Fast 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 ofarray[array.Length - 1]for the last element. Symptom:IndexOutOfRangeExceptionat runtime. Fix: Remember arrays are zero-indexed, so a 5-element array has valid indexes 0–4. Usearray[^1]in C# 8+ for the last element, orarray[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 throwsArgumentException: An item with the same key has already been added. Fix: Always check first withif (!dict.ContainsKey(key))before calling.Add(), or use the indexerdict[key] = valuewhich 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
foreachloop throwsInvalidOperationException: 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
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
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.