Back to Blog
C#

C# List Usage: Practical Guide to List<T> Operations

c# list usage: Learn how to use List<T> in C#: creation, adding, removing, iterating, sorting, and performance considerations for real-world code.

C# ListList<T>CollectionsLINQPerformance
Illustration of C# List<T> operations showing add, remove, and iterate actions.

List<T> is the default choice for a resizable collection in C#. This article focuses on practical c# list usage, covering creation, manipulation, and performance considerations.

Creating and Initializing a List<T>

You can create a List<T> with a default capacity, a specified capacity, or from an existing collection. The simplest form is new List<T>(), which creates an empty list. If you know the approximate number of elements, you can pass an initial capacity to avoid resizing overhead during the initial population.

List<int> numbers = new List<int>(); List<string> names = new List<string>(10); List<int> fromArray = new List<int>(new int[] { 1, 2, 3 });

The capacity parameter does not limit the list size; it only sets the initial internal array size. The list grows automatically as needed, but resizing copies the entire array, so providing a reasonable initial capacity can reduce allocations.

Adding and Removing Elements

Add appends an element to the end of the list. AddRange adds multiple elements at once. For inserting at a specific index, use Insert or InsertRange. Removal can be done with Remove, which removes the first occurrence of a specific value, or RemoveAt to remove by index. RemoveAll removes all elements that match a predicate.

List<string> fruits = new List<string> { "apple", "banana" }; fruits.Add("cherry"); fruits.AddRange(new[] { "date", "elderberry" }); fruits.Insert(1, "blueberry"); fruits.Remove("banana"); fruits.RemoveAt(0); fruits.RemoveAll(f => f.StartsWith("e"));

Each operation has different runtime costs. Add is typically O(1) amortized, but can be O(n) when the internal array resizes. Insert and RemoveAt are O(n) because they shift elements. Remove also requires a linear search to find the element, making it O(n). For frequent insertions or removals at the beginning, consider a LinkedList<T> instead.

Iterating and Accessing Items

Accessing an element by index is O(1) with the [] operator. Iterating with foreach is the most readable way to process all elements. You can also use a for loop if you need the index for other operations.

List<int> scores = new List<int> { 90, 85, 92, 78 }; for (int i = 0; i < scores.Count; i++) { Console.WriteLine($"Score {i}: {scores[i]}"); } foreach (int score in scores) { Console.WriteLine(score); }

Modifying the list while iterating with foreach throws an InvalidOperationException because the list's version changes. If you need to remove elements during iteration, iterate backwards with a for loop or use RemoveAll with a predicate.

Searching and Sorting

List<T> provides Contains, IndexOf, and Find methods for searching. Contains and IndexOf use the default equality comparer, while Find uses a predicate. For custom search logic, FindAll returns a new list with all matches.

List<Person> people = GetPeople(); Person firstAdult = people.Find(p => p.Age >= 18); List<Person> adults = people.FindAll(p => p.Age >= 18); int index = people.IndexOf(firstAdult);

Sorting is done with the Sort method, which uses the default comparer for the type. You can provide a custom IComparer<T> or a Comparison<T> delegate. For LINQ-based ordering, use OrderBy and OrderByDescending, which return a new sorted sequence rather than modifying the original list.

people.Sort((x, y) => string.Compare(x.LastName, y.LastName)); var sortedByName = people.OrderBy(p => p.LastName).ToList();

Sort is an in-place sort with O(n log n) average complexity. OrderBy uses a stable sort and requires additional memory for the new sequence.

Performance and Memory Behavior

The internal implementation of List<T> is an array that is resized as needed. The default growth factor doubles the capacity when the array is full. This means that repeatedly adding a single element can cause multiple array allocations and copies. If you know the final size, use the constructor that accepts a capacity.

Memory usage is proportional to the capacity, not the count. If you have a large list and want to free unused memory, you can call TrimExcess() to reduce capacity to the current count. However, this can be expensive if you plan to add more elements later.

For read-only scenarios, consider using IReadOnlyList<T> as the return type to prevent accidental modification. This does not change the underlying list but restricts the caller's ability to modify it.

Common Pitfalls and How to Avoid Them

One common mistake is modifying a list while iterating with foreach. This throws an exception. Use a for loop in reverse or collect items to remove in a separate list and then remove them.

Another pitfall is using List<T> for frequent insertions at the beginning. Since Insert(0, item) shifts all elements, this is O(n). For a queue-like behavior, use Queue<T>; for frequent middle insertions, consider LinkedList<T>.

When using Remove with a custom class, ensure that Equals and GetHashCode are properly overridden if you rely on value equality. Otherwise, Remove will only remove the exact reference.

Also, be careful with List<T> in multi-threaded scenarios. List<T> is not thread-safe. If multiple threads access the same list concurrently, you need external synchronization or use a ConcurrentBag<T> or ConcurrentQueue<T> depending on the access pattern.

c# list usage: Practical Usage and Code Examples | RYUSLOG DEV