List<T>
The growable ordered collection used far more often than an array: Add, Insert, Remove, RemoveAt, Contains and Count.
List<string> names = new List<string>();A List<T> stores values in order, like an array, except that its length changes as you use it. You add items and it grows. You remove items and it shrinks. Most of the time this is the collection to reach for. A plain array is for when the length is fixed and known in advance.
What the angle brackets mean
The T is a blank you fill in with the type you want to store. List<string> holds strings and nothing else. List<int> holds integers and nothing else. Adding the wrong type is a compile error, not a surprise later.
The members you will use
Add(item)puts an item at the end.Insert(index, item)puts it at a position and shifts the rest right.Remove(item)removes the first match and returnstrueif it found one.RemoveAt(index)removes by position instead of by value.Contains(item)answers yes or no;IndexOf(item)answers where, or-1.Countis how many items there are, so positions run from 0 toCount - 1.
Shorthand
These three lines build the same list:
List<int> a = new List<int> { 1, 2 };
List<int> b = new() { 1, 2 }; // the type is already on the left
List<int> c = [1, 2]; // collection expression, C# 12 and later
Where it goes wrong
Count is the number of items. Capacity is how much room is reserved for more, and it is a different number. Reading a position that does not exist throws ArgumentOutOfRangeException, not the IndexOutOfRangeException an array throws.
Example
List<string> guests = new List<string>(); // starts empty and grows as you add
guests.Add("ana");
guests.Add("bo");
guests.Add("cy");
guests.Insert(1, "dee"); // "dee" takes position 1, the rest shift right
bool removed = guests.Remove("cy"); // Remove reports whether it found the value
guests.RemoveAt(0); // RemoveAt takes a position, not a value
Console.WriteLine($"count: {guests.Count}");
Console.WriteLine($"items: {string.Join(", ", guests)}");
Console.WriteLine($"removed cy: {removed}");
Console.WriteLine($"has bo: {guests.Contains("bo")}");
Console.WriteLine($"bo is at: {guests.IndexOf("bo")}");
// Output:
// count: 2
// items: dee, bo
// removed cy: True
// has bo: True
// bo is at: 1When to use it
- Collecting rows read from a database before returning them from a method.
- Building up a list of validation messages while checking a submitted form.
- Holding the items in a shopping basket, where the count changes with every click.
More examples
Removing matching items by counting backwards
This is the standard fix for the most common List<T> bug. A forwards loop that removes items skips the item after each removal, because everything behind it slid down one position.
List<int> readings = new List<int> { 4, 7, 10, 13, 16 };
// Walking backwards means that removing a position never shifts one you
// have not visited yet. Removing during a foreach over the same list throws.
for (int i = readings.Count - 1; i >= 0; i--)
{
if (readings[i] % 2 == 0)
{
readings.RemoveAt(i);
}
}
Console.WriteLine($"left: {string.Join(", ", readings)}");
Console.WriteLine($"count: {readings.Count}");
// Output:
// left: 7, 13
// count: 2Sort, Reverse and ToArray
Sort and Reverse change the list itself rather than handing back a new one, which surprises people who expect a result to assign. ToArray is the opposite: it copies, so later edits to one side do not reach the other.
List<string> words = new List<string>();
words.AddRange(new[] { "pear", "apple", "plum" }); // adds every item of another collection
words.Sort(); // reorders this list in place and returns nothing
Console.WriteLine($"sorted: {string.Join(", ", words)}");
words.Reverse(); // also in place
Console.WriteLine($"reversed: {string.Join(", ", words)}");
string[] asArray = words.ToArray(); // a copy, not a view
asArray[0] = "quince";
Console.WriteLine($"list still: {words[0]}");
Console.WriteLine($"array now: {asArray[0]}");
// Output:
// sorted: apple, pear, plum
// reversed: plum, pear, apple
// list still: plum
// array now: quinceContains, IndexOf and the indexer together
Reach for this shape when you are keeping an ordered working set and need to check membership before changing it. Guard any use of IndexOf against -1 before using the result as an index.
List<string> queue = new List<string> { "ana", "bo", "cy" };
// Contains answers yes or no. IndexOf answers where, or -1 when absent.
if (!queue.Contains("dee"))
{
queue.Insert(0, "dee");
}
int where = queue.IndexOf("cy");
queue[where] = "cy (paid)"; // the indexer both reads and writes one position
Console.WriteLine(string.Join(" | ", queue));
Console.WriteLine($"cy was at position {where}");
// Output:
// dee | ana | bo | cy (paid)
// cy was at position 3
Discussion