Working with Lists

Vaibhav • September 10, 2025

In the previous section, we introduced generic collections and saw how they improve on arrays by being type-safe and dynamic. Among all generic collections, the most commonly used is the List<T>. Think of it as a more flexible array - it can grow and shrink automatically, provides powerful built-in methods, and is optimized for most everyday scenarios.

What is a List?

A List<T> is a dynamic array. It stores elements in contiguous memory like a normal array, but behind the scenes it manages resizing automatically when you add more items than it can hold. This makes it perfect for cases where the number of elements is not known in advance.

List<T> belongs to the System.Collections.Generic namespace. It is generic, which means you must specify the type of elements it will store (e.g., List<int>, List<string>).

Declaring and Initializing a List

Creating a list is simple. Here are a few common ways:

// Declaring an empty list
List<string> names = new List<string>();

// Declaring with initial capacity
List<int> numbers = new List<int>(10);

// Declaring with initializer syntax
List<char> vowels = new List<char> { 'a', 'e', 'i', 'o', 'u' };

Unlike arrays, you don’t need to know the exact size in advance. If the list runs out of space, it automatically resizes itself.

Adding and Removing Elements

Lists provide several methods to manage elements efficiently. Let’s look at some of the most common ones:

List<string> fruits = new List<string>();

// Adding elements
fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Cherry");

// Inserting at a specific index
fruits.Insert(1, "Mango"); // Apple, Mango, Banana, Cherry

// Removing by value
fruits.Remove("Banana");   // Apple, Mango, Cherry

// Removing by index
fruits.RemoveAt(0);        // Mango, Cherry

Each of these operations is carefully optimized under the hood. For example, Remove will find the first matching element and shift the remaining elements to fill the gap.

Accessing Elements

Just like arrays, lists use zero-based indexing. You can retrieve elements using the indexer syntax:

Console.WriteLine(fruits[0]); // Output: Mango

You can also loop through the list using for or foreach.

// Using for loop
for (int i = 0; i < fruits.Count; i++)
{
    Console.WriteLine(fruits[i]);
}

// Using foreach
foreach (string fruit in fruits)
{
    Console.WriteLine(fruit);
}

Lists expose a Count property (number of elements) - this is different from arrays, which use Length.

Capacity vs Count

One of the most interesting aspects of lists is how they manage memory. A list maintains two important values:

  • Count: The number of actual elements in the list.
  • Capacity: The number of elements the list can hold before resizing.
List<int> numbers = new List<int>();
Console.WriteLine(numbers.Count);    // 0
Console.WriteLine(numbers.Capacity); // 0 (initially)

// Adding elements
numbers.Add(1);
numbers.Add(2);
Console.WriteLine(numbers.Count);    // 2
Console.WriteLine(numbers.Capacity); // 4 (resized automatically)

Lists typically double their capacity each time they run out of space. This amortized resizing strategy ensures good performance over time, even though an individual resize can be costly.

Although resizing involves creating a new larger array and copying elements, the doubling strategy means that on average, adding elements to a list is still an O(1) operation.

Common List Methods

Lists provide a rich set of methods to manipulate elements. Some of the most useful include:

  • Contains(item) - checks if an element exists.
  • IndexOf(item) - returns the index of the first occurrence.
  • Sort() - sorts elements in ascending order.
  • Reverse() - reverses the order of elements.
  • Clear() - removes all elements.
List<int> data = new List<int> { 3, 1, 4, 2 };
data.Sort();        // 1, 2, 3, 4
data.Reverse();     // 4, 3, 2, 1

Performance Considerations

Lists are efficient but not perfect for every scenario. Some key points to remember:

  • Indexing and appending are O(1) on average.
  • Inserting/removing in the middle is O(n) because elements must be shifted.
  • Resizing is costly but happens infrequently thanks to the doubling strategy.

If you know the approximate number of elements you’ll need, initialize the list with a capacity to avoid repeated resizing. For example: new List<int>(1000).

Lists vs Arrays

Both lists and arrays have their place. Here’s a quick comparison:

  • Arrays: Faster for fixed-size data, slightly less memory overhead, no resizing.
  • Lists: Flexible, built-in methods, better for dynamic or unpredictable data sets.

For example, storing sensor readings for a single day (fixed count) might be best with an array, but storing chat messages in an app (unknown count) works better with a list.

Summary

List<T> is one of the most versatile and widely used collections in C#. It combines the speed of arrays with the flexibility of dynamic resizing, while also providing powerful built-in methods. Understanding how lists manage count vs capacity, and their performance implications, will help you use them effectively in real-world applications. In the next article, we’ll explore another powerful collection - Dictionary<K,V>, which introduces key-value pair storage for even faster lookups.