C# Collections Essentials: Arrays & Lists
In our programming journey so far, each variable has held just a single piece of information – one number, one string, one boolean. But what happens when you need to work with a group of related items?
Imagine needing to keep track of all the error codes from an API response, a list of usernames for a test run, or the prices of all items in a shopping cart. Creating a separate variable for each one would be unmanageable and inflexible.
This is where collections come in. In this lesson, we will explore the fundamental ways C# allows you to store and manage groups of data using the foundational Array and the incredibly versatile List<T>.
Why Do We Need to Group Data
The need to handle multiple, related pieces of data is incredibly common in programming. Trying to manage each piece with a separate variable quickly becomes impractical:
- It makes your code verbose and repetitive.
- It's difficult to perform operations on all items collectively (like finding the average score or searching for a specific username).
- It's not scalable – what if you need to store 1000 items instead of 10? You wouldn't want to declare 1000 variables!
This is where collection data structures come in. They allow you to store multiple values under a single name, making it much easier to organize, access, and manage groups of related data. For our test automation, imagine storing a list of expected error messages, a set of test user credentials, or URLs for different environments.
Collections are fundamental for organizing data effectively, leading to cleaner, more efficient, and more maintainable code.
Arrays – Fixed-Size Groups of a Single Type
One of the most basic ways to store a collection of items in C# is by using an Array. An array is a data structure that holds a fixed-size sequential collection of elements, and all elements in an array must be of the same data type.
Think of an array like a numbered row of mailboxes 📦📦📦. Each compartment can hold one item, they are all the same type of compartment, and the total number of compartments is fixed when you get it.
Declaring an Array
You declare an array by specifying the data type of its elements, followed by square brackets [], and then the array name:
int[] scores; // Declares an array variable that can hold integers
string[] userNames; // Declares an array variable that can hold strings
Initializing an Array
Declaring an array variable doesn't create the array itself yet; it just sets aside a name for it. To actually create the array and allocate memory, you use the new keyword and specify its size. Once an array's size is set, it cannot be changed.
// Initialize an array of 5 integers
// All elements will be set to their default value (0 for int)
scores = new int[5];
// You can also declare and initialize in one step:
double[] prices = new double[10]; // An array to hold 10 double values
You can also initialize an array with a specific set of values when you declare it:
string[] browserNames = { "Chrome", "Firefox", "Edge", "Safari" };
// or more explicitly:
int[] initialScores = new int[] { 95, 88, 100, 76 };
Accessing Array Elements
Elements in an array are accessed using their index, which is a zero-based number (meaning the first element is at index 0, the second at index 1, and so on). You use square brackets [] with the index number:
// Continuing with browserNames array from above:
string firstBrowser = browserNames[0]; // Accesses "Chrome"
string thirdBrowser = browserNames[2]; // Accesses "Edge"
Console.WriteLine($"The first browser is: {firstBrowser}");
// You can also assign new values to elements
browserNames[1] = "Firefox ESR"; // Changes the second element
Be careful! Trying to access an element using an index that's out of bounds (e.g., browserNames[4] when the array only has 4 elements with indices 0, 1, 2, 3) will result in a runtime error (an IndexOutOfRangeException).
Getting the Array Length
You can find out how many elements an array can hold using its Length property:
int numberOfBrowsers = browserNames.Length; // numberOfBrowsers will be 4
Console.WriteLine($"We support {numberOfBrowsers} browsers.");
Looping Through an Array
A common task is to iterate through all the elements in an array. The for loop is perfect for this, using the array's Length property:
Console.WriteLine("Supported Browsers:");
for (int i = 0; i < browserNames.Length; i++)
{
Console.WriteLine($"- {browserNames[i]}");
}
Arrays are efficient for storing and accessing a known number of items.
The Limitation – Array Sizes Are Set in Stone
The defining characteristic of an array, and its main limitation for some scenarios, is its fixed size. When you create an array, like new int[5], you are reserving space for exactly five integers. You can't later decide you need six integers and just expand that same array. Similarly, you can't shrink it if you only end up using three.
This can be problematic if:
- You don't know how many items you'll need to store at the time you're writing the code.
- The number of items needs to change frequently during program execution (e.g., users adding or removing items from a list).
Imagine an egg carton designed to hold exactly six eggs. It's efficient for six eggs, but trying to squeeze in a seventh is a no-go, and it feels wasteful if you only have two eggs. For situations requiring more flexibility, C# offers other collection types.
List<T> – Flexible Dynamic Collections
When you need a collection of items that can grow or shrink dynamically as your program runs, the List<T> class is often your best friend. It's part of the System.Collections.Generic namespace (so you'll usually add using System.Collections.Generic; at the top of your C# file).
The <T> part might look a bit strange at first. This signifies that List is a generic type. T is a placeholder for the actual data type of the items the list will hold. This makes List<T> type-safe – a List<string> can only hold strings, and a List<int> can only hold integers.
Declaring and Initializing a List><T>
using System.Collections.Generic; // Don't forget this!
// ... inside your class or method ...
// Create an empty list that can hold strings
List<string> studentNames = new List<string>();
// Create a list and initialize it with some integer scores
List<int> recentScores = new List<int> { 95, 82, 100, 91 };
Common List<T> Operations
List<T> comes with many convenient methods for managing its items:
.Add(item): Adds an item to the end of the list.studentNames.Add("Alice"); studentNames.Add("Bob");.IndexOf(T): Searches for a specific value in the list and returns its first occurrence's index.int nameIndex = studentNames.IndexOf("Bob"); if (nameIndex != -1) // IndexOf returns -1 if an item that matches the condition is not found { Console.WriteLine($"Found 'Bob' at index {nameIndex}."); }.Insert(int index, T item): Inserts an element into the list at the specified index.studentNames.Insert(1, "John"); // Inserts "John" between "Alice" and "Bob" items.Remove(item): Removes the first occurrence of a specific item from the list. Returnstrueif removed,falseotherwise.studentNames.Remove("Bob");.RemoveAt(index): Removes the item at the specified zero-based index.recentScores.RemoveAt(1); // Removes the score 82- Accessing Elements (by Index): Just like arrays, you can access elements using their index with square brackets
[].string firstStudent = studentNames[0]; // Accesses "Alice" .Countproperty: Gets the number of items currently in the list (similar to.Lengthfor arrays).int numberOfStudents = studentNames.Count;.Clear(): Removes all items from the list..Contains(item): Checks if the list contains a specific item. Returnstrueorfalse..Sort(): Sorts the elements in the list using the default comparer..Reverse(): Reverses the order of the elements in the list.
Looping Through a List<T>
You can use a for loop with .Count, just like with arrays. However, the foreach loop (which we peeked at earlier) is often more convenient for lists:
List<string> tasks = new List<string>();
tasks.Add("Prepare lesson outline");
tasks.Add("Write C# examples");
tasks.Add("Review content");
Console.WriteLine("Upcoming Tasks:");
foreach (string task in tasks)
{
Console.WriteLine($"- {task}"); // Using string interpolation
}
Console.WriteLine($"Total tasks: {tasks.Count}");
List<T> provides a great balance of power and ease of use for managing dynamic collections.
Under the Hood: List<T> Performance
To understand how List<T> works so flexibly, you need to know about two of its properties: Count and Capacity.
- Count: This is simple. It's the number of elements that are actually in the list. If you add 3 items, the Count is 3.
- Capacity: This is more interesting. A
List<T>uses an array internally to store its elements. The Capacity is the actual size of that internal array – the number of elements it can hold before it needs to be resized.
When you create a new empty list, it starts with a small capacity (say, 4). When you add the 5th item, the Count exceeds the Capacity. At this point, the list must perform a resizing operation: it creates a new, larger array (often doubling the capacity), copies all the elements from the old array to the new one, and then discards the old array. This resizing can be a small performance cost.
A Note on Performance
If you know ahead of time that you are going to add a large number of items to a list (e.g., 10,000 records from a file), you can give the list a hint by setting its initial capacity. This avoids multiple, inefficient resizing operations.
// This allocates an internal array with space for 10,000 items upfront.
List<string> testData = new List<string>(10000);
For most day-to-day test automation scenarios, the default resizing behavior is perfectly fine and not something you need to worry about. But understanding this "Capacity vs. Count" mechanic gives you a deeper insight into how List<T> works.
Working Between Arrays and Lists
Because arrays and lists are so fundamental, C# makes it very easy to convert between them.
List to Array
If you have a List<T> and need to pass it to a method that specifically requires an array, you can use the convenient .ToArray() method.
List<string> myTestCases = new List<string> { "Test1", "Test2", "Test3" };
string[] testArray = myTestCases.ToArray(); // Converts the list into a new string array
Console.WriteLine(testArray.Length); // Output: 3
Array to List
If you start with a fixed array of data but realize you need the flexibility to add or remove items, you can easily create a List<T> from it by passing the array into the list's constructor.
string[] initialBrowsers = { "Chrome", "Firefox" };
List<string> browserList = new List<string>(initialBrowsers);
// Now we can use List methods!
browserList.Add("Edge");
Console.WriteLine(browserList.Count); // Output: 3
This flexibility is very useful when interacting with different libraries or APIs that might expect one type or the other.
Handling Common Collection Errors
As you start working with arrays and lists to manage groups of data, you'll inevitably encounter a few common types of runtime exceptions. Understanding why they happen is the first step to preventing them and writing more robust code. Let's look at the usual suspects.
IndexOutOfRangeException with Arrays
This is a classic error. It occurs when you try to access an array element using an index that doesn't exist. Remember that arrays are zero-based, so for an array of size 3, the only valid indices are 0, 1, and 2.
string[] browserNames = { "Chrome", "Firefox", "Edge" }; // Length is 3, valid indices are 0, 1, 2.
Console.WriteLine(browserNames[0]); // Works fine, prints "Chrome"
try
{
// Trying to access the element at index 3, which doesn't exist.
Console.WriteLine(browserNames[3]);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("Oops! That index is out of bounds.");
Console.WriteLine(ex.Message); // "Index was outside the bounds of the array."
}
How to avoid it: Always check that the index you're using is greater than or equal to 0 AND less than the array's Length property, especially when working with loops or calculated indices.
string[] browserNames = { "Chrome", "Firefox", "Edge" };
int requestedIndex = 3; // Trying to access index 3 (out of bounds)
if (requestedIndex >= 0 && requestedIndex < browserNames.Length)
{
Console.WriteLine($"Browser at index {requestedIndex}: {browserNames[requestedIndex]}");
}
else
{
Console.WriteLine($"Invalid index {requestedIndex}. Valid indices are from 0 to {browserNames.Length - 1}.");
}
ArgumentOutOfRangeException with Lists
This is the List<T> equivalent of the previous error. It's often thrown when you try to use an index-based method like RemoveAt() or Insert() with an index that is negative or greater than or equal to the list's Count.
List<string> tasks = new() { "Write Tests", "Refactor Code" }; // Count is 2, valid indices are 0, 1.
try
{
tasks.RemoveAt(2); // Trying to remove an item at index 2, which doesn't exist.
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine("Error! That's not a valid index for the list.");
Console.WriteLine(ex.Message); // "Index must be within the bounds of the List."
}
NullReferenceException with Collections
This infamous exception can happen if you declare a collection variable but forget to initialize it (create an actual instance of it with new) before trying to use it.
List<string> testUsers; // Declared, but currently null (doesn't point to a real list object)
try
{
// This will throw a NullReferenceException because 'testUsers' is null.
testUsers.Add("myuser");
}
catch (NullReferenceException ex)
{
Console.WriteLine("Error! The list was never created.");
Console.WriteLine(ex.Message); // "Object reference not set to an instance of an object."
}
// The fix is to always initialize it:
List<string> validTestUsers = new List<string>();
validTestUsers.Add("myuser"); // This works perfectly!
Being mindful of collection boundaries and ensuring your collection variables are properly initialized will save you from these very common runtime errors.
Arrays vs. List<T> – When to Use Which
Both arrays and List<T> are used for storing collections of items of the same type. So, when should you choose one over the other?
Use Arrays when:
- You know the exact number of elements your collection will hold at the time you create it, and this size will not change.
- You are working with a very performance-critical section of code where the slight overhead of
List<T>might theoretically matter (though for most application and test automation code, this difference is negligible). - You are interacting with older APIs or libraries that specifically require an array as input or output.
An example could be storing the days of the week: there are always seven, and that's fixed.
string[] daysOfWeek = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
Use List<T> when:
- You don't know how many items you'll need to store when you create the collection.
- The number of items in the collection is expected to change frequently (items being added or removed).
- You want the convenience of built-in methods like
Add(),Remove(),Sort(),Find(), etc., without having to implement that logic yourself.
For many common tasks in test automation – like storing a list of web elements found on a page (which can vary), a list of test data records read from a file, or a list of error messages collected during a test run – List<T> is typically the more practical and flexible choice.
Beyond Arrays and Lists – A Peek Ahead
Arrays and List<T> are your foundational tools for handling collections in C#. They will cover a vast majority of your needs as you start out. However, it's good to be aware that the .NET Base Class Library (specifically in namespaces like System.Collections.Generic) provides many other specialized collection types designed for different purposes. You don't need to master these now, but here are a few names you might encounter later:
Dictionary<TKey, TValue>: Stores a collection of key-value pairs. Think of it like a real-world dictionary where you look up a word (the key) to find its definition (the value). Super useful for fast lookups based on a unique key.HashSet<T>: Stores a set of unique items. If you try to add an item that's already in the set, it won't be added again. Good for quickly checking if an item exists in a collection or for removing duplicates.Queue<T>: A "First In, First Out" (FIFO) collection. Items are added to the end (enqueued) and removed from the beginning (dequeued), like a checkout line at a store.Stack<T>: A "Last In, First Out" (LIFO) collection. Items are added to the top (pushed) and removed from the top (popped), like a stack of plates.
For now, getting really comfortable with arrays (for fixed-size needs) and especially List<T> (for most dynamic collection needs) will set you up perfectly for the next stages of your C# and test automation journey!
Key Takeaways
- Arrays in C# are used to store a fixed-size, sequential collection of elements, all of the same data type. Elements are accessed via a zero-based index.
- The primary limitation of arrays is their fixed size; once created, an array cannot grow or shrink.
List<T>provides a dynamic, resizable collection that can hold elements of a specific typeT.List<T>offers convenient methods likeAdd(),Remove(),RemoveAt(), and uses aCountproperty to determine the number of items.- For situations where the collection size needs to change or is unknown beforehand,
List<T>is generally more flexible and recommended over arrays. - Understanding the difference between a list's internal
Capacityand its publicCountis key to appreciating its performance characteristics. - You can easily convert between Arrays and
List<T>using the.ToArray()method or theList<T>constructor. - Understanding common collection errors is essential for writing robust and error-free code. Always verify index boundaries before accessing elements, ensure lists are properly initialized, and handle dynamic collections carefully to avoid runtime failures.
- .NET provides many other specialized collection types (like Dictionaries, HashSets, Queues, Stacks) for more advanced data storage needs.
Mastering Collections
- Microsoft Docs: Arrays (C# Programming Guide) The official guide to working with arrays in C#.
- Microsoft Docs: List<T> Class The definitive documentation with a full list of all available methods and properties.
- Microsoft Docs: Array Class Explore the static helper methods available for working with arrays.