What Is List<T> in C#?
List<T> is a generic collection in C# that represents a
dynamic array. It stores elements in an indexed sequence
and can automatically grow as new elements are added.
Unlike a fixed-size C# array, a List<T> does not require
the final number of elements to be known when the collection is created.
It is commonly used when an application needs an ordered collection whose
size can change during execution.
Creating a List<T>
To use List<T>, include the
System.Collections.Generic namespace. The type parameter
T specifies the type of elements that the list can contain.
using System.Collections.Generic;
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
The list above can contain only int values. Generic typing
provides compile-time type safety.
Initializing a List with Values
A list can be initialized with values when it is created.
var fruits = new List<string>
{
"Apple",
"Banana",
"Orange"
};
Console.WriteLine(fruits[0]);
// Apple
The elements are stored in insertion order and can be accessed using zero-based indexes.
Adding Elements with Add()
The Add() method appends an element to the end of the list.
If the internal storage has no available capacity, the list can allocate
a larger backing array and copy its existing elements.
var numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
foreach (var number in numbers)
{
Console.WriteLine(number);
}
Adding Multiple Elements with AddRange()
AddRange() adds multiple elements from another collection to
the end of the list.
var numbers = new List<int>
{
10,
20
};
numbers.AddRange(new[] { 30, 40, 50 });
Console.WriteLine(string.Join(", ", numbers));
// 10, 20, 30, 40, 50
Inserting Elements with Insert()
The Insert() method places an element at a specified index.
Existing elements from that position onward are shifted to make room.
var numbers = new List<int>
{
10,
20,
40
};
numbers.Insert(2, 30);
Console.WriteLine(string.Join(", ", numbers));
// 10, 20, 30, 40
Inserting near the beginning of a large list can require many elements to
be shifted, making the operation O(n) in the general case.
Accessing Elements by Index
A List<T> provides direct indexed access. Because the
collection is backed by an array, accessing an element by a valid index is
typically O(1).
var languages = new List<string>
{
"C#",
"Java",
"Python",
"JavaScript"
};
Console.WriteLine(languages[2]);
// Python
Updating an Element
An existing element can be replaced by assigning a new value to its index.
var languages = new List<string>
{
"C#",
"Java",
"Python"
};
languages[1] = "TypeScript";
Console.WriteLine(string.Join(", ", languages));
// C#, TypeScript, Python
Removing Elements from a List
C# provides several methods for removing elements depending on whether you want to remove by value, index, or condition.
var numbers = new List<int>
{
10,
20,
30,
40
};
numbers.Remove(20);
numbers.RemoveAt(1);
Console.WriteLine(string.Join(", ", numbers));
// 10, 40
Remove() removes the first matching value, while
RemoveAt() removes the element at a specified index.
RemoveRange() and Clear()
RemoveRange() removes a specified number of elements starting
at an index. Clear() removes all elements from the list.
var numbers = new List<int>
{
10,
20,
30,
40,
50
};
numbers.RemoveRange(1, 2);
Console.WriteLine(string.Join(", ", numbers));
// 10, 40, 50
numbers.Clear();
Console.WriteLine(numbers.Count);
// 0
Checking Whether a List Contains an Element
The Contains() method checks whether a value exists in the
list.
var numbers = new List<int>
{
10,
20,
30
};
Console.WriteLine(numbers.Contains(20));
// True
Console.WriteLine(numbers.Contains(50));
// False
For a general List<T>, Contains() performs
a linear search and therefore takes O(n)
Finding Elements with Find()
Find() returns the first element that satisfies a specified
condition. This is particularly useful when a list contains objects.
var numbers = new List<int>
{
5,
12,
18,
25
};
int result = numbers.Find(number => number > 15);
Console.WriteLine(result);
// 18
FindIndex() and FindAll()
FindIndex() returns the index of the first matching element,
while FindAll() returns all elements that satisfy a
condition.
var numbers = new List<int>
{
10,
15,
20,
25,
30
};
int index = numbers.FindIndex(number => number == 20);
var evenNumbers = numbers.FindAll(number => number % 2 == 0);
Console.WriteLine(index);
// 2
Console.WriteLine(string.Join(", ", evenNumbers));
// 10, 20, 30
Iterating Through a List
A list can be traversed using foreach, a traditional
for loop, or LINQ when a transformation or filtering
operation is required.
var languages = new List<string>
{
"C#",
"Java",
"Python"
};
foreach (var language in languages)
{
Console.WriteLine(language);
}
Use a for loop when the index itself is important.
for (int i = 0; i < languages.Count; i++)
{
Console.WriteLine($"{i}: {languages[i]}");
}
Sorting a List
The Sort() method sorts the elements of a list according to
their default comparison behavior or a supplied comparer.
var numbers = new List<int>
{
40,
10,
30,
20
};
numbers.Sort();
Console.WriteLine(string.Join(", ", numbers));
// 10, 20, 30, 40
Sorting in Descending Order
A custom comparer can be passed to Sort() when a different
ordering is required.
var numbers = new List<int>
{
40,
10,
30,
20
};
numbers.Sort((a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", numbers));
// 40, 30, 20, 10
List Capacity and Count
Count and Capacity describe different aspects
of a List<T>.
- Count: number of elements currently stored.
- Capacity: amount of storage allocated internally for elements.
var numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
Console.WriteLine($"Count: {numbers.Count}");
Console.WriteLine($"Capacity: {numbers.Capacity}");
Capacity may be larger than count because the list keeps additional
internal storage available for future additions.
Setting an Initial Capacity
If the approximate number of elements is already known, an initial
capacity can be supplied to reduce the number of internal reallocations
while the list grows.
var users = new List<string>(100);
Console.WriteLine(users.Capacity);
// 100
Initial capacity does not add 100 elements to the list. The
Count remains zero until values are actually added.
TrimExcess()
A list may have more capacity than it currently needs. The
TrimExcess() method can be used when you want to reduce
excess allocated capacity.
var numbers = new List<int>(100);
numbers.Add(10);
numbers.Add(20);
Console.WriteLine(numbers.Count);
// 2
numbers.TrimExcess();
Console.WriteLine(numbers.Capacity);
Trimming capacity is not something that should normally be performed
after every removal. It is more appropriate when the collection has
reached a stable size and reducing unused storage is useful.
Converting a List to an Array
The ToArray() method creates an array containing the current
elements of the list.
var numbers = new List<int>
{
10,
20,
30
};
int[] array = numbers.ToArray();
Console.WriteLine(array.Length);
// 3
Working with Objects
List<T> is especially useful for storing collections of
custom objects.
class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
var students = new List<Student>
{
new Student { Id = 1, Name = "Asha" },
new Student { Id = 2, Name = "Rahul" },
new Student { Id = 3, Name = "Priya" }
};
var student = students.Find(s => s.Id == 2);
Console.WriteLine(student?.Name);
// Rahul
Filtering a List with LINQ
LINQ can be used with List<T> to filter, project, group,
and transform collection data.
using System.Linq;
var numbers = new List<int>
{
5,
10,
15,
20,
25
};
var result = numbers
.Where(number => number >= 15)
.ToList();
Console.WriteLine(string.Join(", ", result));
// 15, 20, 25
List<T> Time Complexity
Because List<T> is backed by an array, its common
operations have predictable performance characteristics.
Operation
Typical Complexity
Access by index
O(1)
Update by index
O(1)
Add at end
O(1) amortized
Insert at beginning/middle
O(n)
Remove by index
O(n) in general
Contains
O(n)
Find
O(n)
When Should You Use List<T>?
List<T> is a good choice when you need an ordered,
resizable collection and frequently access elements by index or append
values to the end.
- Lists of users or products.
- Search results and API response collections.
- Application records that grow during execution.
- Collections that require indexed access.
- Data that needs sorting or filtering.
- Temporary collections used during processing.
List<T> vs Array
Both arrays and List<T> provide indexed access, but
their sizing behavior is different.
Feature
Array
List<T>
Size
Fixed
Resizable
Index access
O(1)
O(1)
Append
Not directly supported beyond capacity
O(1) amortized
Insertion
Requires shifting or a new array
Supported with methods such as Insert()
Convenience
Simple fixed storage
Rich collection API
Common List<T> Mistakes
- Confusing
Count with Capacity.
- Assuming
Contains() performs constant-time lookup.
- Frequently inserting at the beginning of a large list without considering shifting costs.
- Calling
TrimExcess() repeatedly during frequent modifications.
- Using a list when key-based lookup would be better handled by a dictionary.
- Using a list when uniqueness is the primary requirement instead of using a hash set.
- Accessing an invalid index and causing an
ArgumentOutOfRangeException.
Practical Example: Managing Developer Skills
A practical application can use List<T> to maintain an
ordered collection of skills. Elements can be added, searched, filtered,
and sorted as requirements change.
var skills = new List<string>
{
"C#",
"ASP.NET Core",
"SQL"
};
skills.Add("Azure");
skills.Add("Docker");
var hasCSharp = skills.Contains("C#");
var backendSkills = skills
.Where(skill => skill.Contains("ASP.NET") || skill == "C#")
.ToList();
skills.Sort();
Console.WriteLine($"Has C#: {hasCSharp}");
Console.WriteLine(string.Join(", ", backendSkills));
Console.WriteLine(string.Join(", ", skills));
Key Takeaways
List<T> is a generic, resizable collection in C#.
- It provides fast indexed access through an array-backed implementation.
Add() appends an element and is O(1) amortized.
Insert() may require shifting elements and is O(n) in general.
Remove() removes by value, while RemoveAt() removes by index.
Count represents the number of stored elements.
Capacity represents allocated internal storage.
Contains() and Find() generally require linear search.
List<T> is a practical default for many ordered, resizable collections.