LogIn
I don't have account.

Comprehensive Guide to List in C# with Examples

DevSniper

136 Views

#language-concepts

List<T> is one of the most useful collections in C#. It behaves like a dynamic array, is strongly typed and gives you flexible, high-performance APIs for most everyday collection needs.

This article explains what a List is, when to use it, how it works internally, all important constructors and properties and the many ways to create and add elements with clear code examples and practical tips.

Namespace :- System.Collections.Generic
Assembly :- System.Collections.dll
Signature :-
public class List<T> : 
   System.Collections.Generic.ICollection<T>,
   System.Collections.Generic.IEnumerable<T>,
   System.Collections.Generic.IList<T>,
   System.Collections.Generic.IReadOnlyCollection<T>,
   System.Collections.Generic.IReadOnlyList<T>,
   System.Collections.IList

What is List<T>?

List<T> is a generic collection class in the System.Collections.Generic namespace. It stores a sequence of elements of type T and provides.

  • fast index-based access (list[i]).
  • dynamic resizing (grows when you Add more items).
  • many helper methods (Add, Insert, Remove, Sort, Find etc.).
  • type safety (no boxing/unboxing for value types).

When Should You Use List<T> in C#?

The List<T> class is one of the most commonly used generic collections in C#. It provides dynamic resizing, fast indexed access and a rich set of utility methods for managing data efficiently.

Use List<T> when

  • You need an ordered collection with fast random access (O(1) read by index).
  • You expect moderate numbers of inserts at the end (append-heavy workloads).
  • You want a flexible collection with many built-in operations (sort, find, search).
  • You want a simple, efficient default container for most non-concurrent scenarios.

Avoid List<T> when

  • You frequently insert/remove from the start or middle of a very large list (costly shifts -> O(n)). Consider LinkedList<T> or a different data structure.
  • You require thread-safety for concurrent writes use ConcurrentBag<T> / ConcurrentQueue<T> / ConcurrentDictionary<TKey,TValue> as appropriate.
  • You need guaranteed O(1) insertion and deletion in the middle other structures (trees, linked lists) may be better.

How List<T> works internally (simple and practical)

List<T> is implemented as a wrapper around an array

Copy
internal T[] _items;   // backing array
internal int _size;    // number of elements stored (Count)

Key behavior

1. Capacity vs Count

  • Capacity = length of _items array (how many elements it can hold before resizing).
  • Count = actual number of elements stored (_size).

2. Adding items

  • If _size < Capacity: place the item at _items[_size] and increment _size (fast O(1)).
  • If _size == Capacity: resize the internal array (allocate a larger array, copy existing items), then add the item. Resizing is O(n).

3. Growth strategy

  • When capacity is exceeded, List<T> typically grows by doubling the capacity (implementation details may vary by .NET version). Doubling yields amortized O(1) cost for Add.

4. Index access

  • Direct array access: list[i] -> O(1). Index bounds checked (throws ArgumentOutOfRangeException if invalid).

5. Insert and Remove

  • Insert(index, item) and removals shift elements right/left to maintain contiguous storage -> O(n).

6. Enumeration

  • Enumerator walks the backing array up to _size and is fast. Note: modifying the list during enumeration throws InvalidOperationException.

7. Memory implications

  • Backing array may have unused slots (Capacity > Count), so memory footprint can be larger than the actual element count.
  • Use TrimExcess() to reduce capacity to Count if needed, use EnsureCapacity() to preallocate and avoid multiple resizes.

List Constructors in C#

The List<T> class in C# provides three primary constructors, each serving a specific purpose from creating an empty list to preallocating memory for performance optimization.

1. Default Constructor

Creates an empty list with an initial capacity of 0. As elements are added, the internal array automatically grows to accommodate new items.

public List();
Example
Copy
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
Console.WriteLine(string.Join(", ", numbers));
10, 20
  • The internal capacity increases automatically as you add elements.
  • Best for small or unpredictable data sizes.

2. Collection Constructor

Initializes a new list containing all elements copied from another collection (e.g., array, another list or any IEnumerable<T> source).

public List(IEnumerable<T> collection);
Example
Copy
int[] array = { 1, 2, 3, 4 };
List<int> numbers = new List<int>(array);
Console.WriteLine(string.Join(", ", numbers));
1, 2, 3, 4
  • Ideal for cloning or copying elements from an existing collection.
  • The list’s capacity is automatically set to the size of the source collection.

3. Capacity Constructor

Creates an empty list but reserves internal storage for a specified number of elements. This avoids multiple reallocations as elements are added, improving performance for large data sets.

public List(int capacity);
Example
Copy
List<string> names = new List<string>(1000); // Preallocate space for 1000 items
names.Add("Alice");
names.Add("Bob");
Console.WriteLine($"Count: {names.Count}, Capacity: {names.Capacity}");
Count: 2, Capacity: 1000
  • Best when you know the approximate number of items beforehand.
  • Reduces memory reallocation overhead during bulk insertions.
  • You can still exceed the capacity, it auto-expands as needed.

List<T> properties in C#

1. Capacity

Capacity is the size of the internal array used by List<T> i.e. how many elements the list can hold without allocating a new array and copying elements.

public int Capacity { get; set; }
Behavior
  • Capacity >= Count always.
  • Setting Capacity to a value greater than current capacity allocates a new internal array and copies existing elements (O(n)).
  • Setting Capacity to a value less than Count throws ArgumentOutOfRangeException.
  • If you never set it explicitly, the list grows automatically (implementation typically doubles capacity when needed).
  • Useful to avoid repeated reallocations when you know expected size (preallocation).
Complexity
  • Reading: O(1)
  • Increasing capacity (set): O(n) because items are copied.
  • Decreasing capacity (set) to >= Count: O(n) because items are copied.
Example
Copy
var list = new List<int>();             // Capacity is implementation dependent (0 initially in .NET Core)
Console.WriteLine(list.Capacity);       // e.g., 0 (may print 0 or a small default)

list.Capacity = 1000;                   // Preallocate space for 1000 items (costly copy if non-empty)
Console.WriteLine(list.Capacity);       // 1000

// Error: Cannot set Capacity smaller than Count
list.AddRange(Enumerable.Range(1, 10));
try {
    list.Capacity = 5;                  // throws ArgumentOutOfRangeException
} catch (ArgumentOutOfRangeException) {
    Console.WriteLine("Cannot set Capacity < Count");
}
Practical tips
  • Pre-set Capacity when doing large AddRange / many Add() operations.
  • Use TrimExcess() (or set Capacity = list.Count) to shrink capacity after big removals.
  • Avoid setting Capacity repeatedly , batch choose an appropriate value.

2. Count

The number of elements currently stored in the list.

public int Count { get; }
Behavior
  • Always 0 <= Count <= Capacity.
  • Getting Count is cheap (O(1)).
  • Updated on Add, Remove, Insert, Clear etc.
Example
Copy
var list = new List<string> { "a", "b", "c" };
Console.WriteLine(list.Count); // 3

list.RemoveAt(1);
Console.WriteLine(list.Count); // 2
Practical tips
  • Use Count to loop by index (for (int i = 0; i < list.Count; i++)) rather than recalculating list.Count in a slow loop (though Count itself is O(1)).
  • Count is the value to compare against when shrinking Capacity.

3. this[int index] :- Indexer / Item

Array-like access to the element at index. You can both read and assign via list[index].

public T this[int index] { get; set; }
Behavior
  • Index is zero-based.
  • Getting or setting with index < 0 or index >= Count throws ArgumentOutOfRangeException.
  • Setting writes into the existing slot (does not change Count).
  • Accessing time complexity is O(1).
Example
Copy
var list = new List<int> { 10, 20, 30 };
int x = list[1];            // 20
list[1] = 25;               // list now {10, 25, 30}

try {
    var bad = list[3];      // throws ArgumentOutOfRangeException
} catch (ArgumentOutOfRangeException) {
    Console.WriteLine("Index out of range!");
}
Practical tips
  • Use indexer for fast random access and updates.
  • For enumerating with mutation, be careful: modifying structure (e.g. add/remove) while iterating invalidates enumerators.

Creating and Adding Elements in List

Once a List<T> is declared, you need to add elements to it before using or accessing its contents. C# provides multiple intuitive ways to populate a list the most common being collection initializers and the Add() method.

1. Using Collection Initializer

A collection initializer lets you create and fill a list in a single, concise statement, perfect when you already know the elements at declaration time.

Copy
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var list = new List<string> {"C#","Java","C++","Python"};
        Console.WriteLine($" index 0 value : {list[0]} and index 2 value : {list[2]}");
    }
}
index 0 value : C# and index 2 value : C++

2. Using the Add() Method

If you need to build a list dynamically at runtime, the Add() method is the most common and flexible approach.

Copy
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var list = new List<string>();
        list.Add("C#");
        list.Add("Java");
        list.Add("C++");
        list.Add("Python");
        Console.WriteLine($" index 0 value : {list[0]} and index 2 value : {list[2]}");
    }
}
index 0 value : C# and index 2 value : C++

Important Characteristics of List<T>

  • Dynamic Size: Unlike arrays, a List can grow automatically.
  • Strongly Typed: Only elements of type T can be stored.
  • Duplicates Allowed: You can store multiple elements with the same value.
  • Index-Based Access: Elements can be accessed via indices, making it efficient for retrieval.
  • Memory Management: Internally backed by an array. Automatically resizes when capacity is exceeded.