Lists

List<T> is the collection you'll use more than any other. Like an array it keeps items in order and gives fast access by index, but it grows and shrinks as you add and remove items. The <T> means it's generic: List<string> holds strings, List<int> holds integers, and the compiler checks every item.

Creating a list and adding items

C#
var tasks = new List<string>();
tasks.Add("Write report");
tasks.Add("Email Sam");
tasks.Add("Book flights");

List<int> primes = [2, 3, 5, 7];        // collection expression
List<string> copy = new(tasks);         // copy of another collection

Console.WriteLine(tasks.Count);
Console.WriteLine(tasks[0]);
Console.WriteLine(string.Join(", ", primes));
Console.WriteLine(copy.Count);
Output
3
Write report
2, 3, 5, 7
3

Lists use Count, not Length. new(tasks) is a target-typed new: the type comes from the variable declaration.

Adding, inserting and removing

C#
List<string> queue = ["Ana", "Ben", "Cleo"];

queue.Insert(0, "VIP Dan");           // at a position
queue.AddRange(["Eve", "Finn"]);      // several at once
Print(queue);

queue.Remove("Ben");                  // first matching value
queue.RemoveAt(0);                    // by index
Print(queue);

bool removed = queue.Remove("Zoe");   // returns false if not found
Console.WriteLine(removed);

queue.RemoveAll(name => name.StartsWith('E') || name.StartsWith('F'));
Print(queue);

queue.Clear();
Console.WriteLine(queue.Count);

static void Print(List<string> list) => Console.WriteLine($"[{string.Join(", ", list)}]");
Output
[VIP Dan, Ana, Ben, Cleo, Eve, Finn]
[Ana, Cleo, Eve, Finn]
False
[Ana, Cleo]
0

Searching

C#
List<int> temps = [18, 22, 25, 19, 30, 22];

Console.WriteLine(temps.Contains(25));
Console.WriteLine(temps.IndexOf(22));
Console.WriteLine(temps.LastIndexOf(22));
Console.WriteLine(temps.Find(t => t > 20));
Console.WriteLine(temps.FindIndex(t => t > 28));
Console.WriteLine(string.Join(", ", temps.FindAll(t => t >= 22)));
Console.WriteLine(temps.Exists(t => t < 0));
Console.WriteLine(temps.TrueForAll(t => t > 10));
Output
True
1
5
22
4
22, 25, 30, 22
False
True

Sorting

Sort() sorts in place using the natural order. Pass a comparison to sort any other way — a.CompareTo(b) for ascending, b.CompareTo(a) for descending:

C#
List<string> words = ["pear", "Fig", "banana", "kiwi", "apple"];

words.Sort();
Console.WriteLine(string.Join(", ", words));

words.Sort((a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", words));

words.Sort((a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", words));

words.Reverse();
Console.WriteLine(string.Join(", ", words));
Output
apple, banana, Fig, kiwi, pear
Fig, kiwi, pear, apple, banana
pear, kiwi, Fig, banana, apple
apple, banana, Fig, kiwi, pear

The default string sort is culture-aware, so "Fig" sorts with the other F-words rather than before all lower-case letters. LINQ's OrderBy (lesson 49) sorts into a new sequence and leaves the list unchanged.

Lists of objects

Lists usually hold objects. Here a record (lesson 40) defines a small product type:

C#
List<Product> cart =
[
    new("Laptop stand", 34.99m, 1),
    new("USB-C cable", 9.99m, 3),
    new("Webcam", 59.00m, 1),
];

cart.Add(new Product("Mouse pad", 7.50m, 2));

decimal total = 0;
foreach (Product p in cart)
{
    Console.WriteLine($"{p.Name,-14}{p.Quantity} x {p.Price,6}");
    total += p.Price * p.Quantity;
}
Console.WriteLine($"Total: {total}");

record Product(string Name, decimal Price, int Quantity);
Output
Laptop stand  1 x  34.99
USB-C cable   3 x   9.99
Webcam        1 x  59.00
Mouse pad     2 x   7.50
Total: 138.96

Don't modify a list while looping over it

Adding or removing items inside a foreach over the same list throws an exception, because the loop can no longer keep its place:

C#
List<int> numbers = [1, 2, 3, 4, 5, 6];
foreach (int n in numbers)
{
    if (n % 2 == 0)
        numbers.Remove(n);
}
Output
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

Use RemoveAll with a condition, loop backwards with a for loop, or build a new list instead:

C#
List<int> numbers = [1, 2, 3, 4, 5, 6];
numbers.RemoveAll(n => n % 2 == 0);
Console.WriteLine(string.Join(", ", numbers));

List<int> more = [1, 2, 3, 4, 5, 6];
for (int i = more.Count - 1; i >= 0; i--)
{
    if (more[i] % 3 == 0) more.RemoveAt(i);
}
Console.WriteLine(string.Join(", ", more));
Output
1, 3, 5
1, 2, 4, 5

Count and Capacity

Internally a list stores items in an array. When it fills up, the list allocates a bigger array (roughly double the size) and copies everything across. Capacity is the size of that internal array; Count is how many items are actually stored:

C#
var list = new List<int>();
int lastCapacity = -1;
for (int i = 0; i < 20; i++)
{
    list.Add(i);
    if (list.Capacity != lastCapacity)
    {
        Console.WriteLine($"Count {list.Count,2} -> capacity {list.Capacity}");
        lastCapacity = list.Capacity;
    }
}
Output
Count  1 -> capacity 4
Count  5 -> capacity 8
Count  9 -> capacity 16
Count 17 -> capacity 32

If you know roughly how many items are coming, new List<int>(10_000) sets the starting capacity and avoids the repeated copying.

Converting and combining

C#
List<int> a = [1, 2, 3];
int[] b = [4, 5];

int[] asArray = a.ToArray();
List<int> asList = b.ToList();
List<int> combined = [.. a, .. b, 6];     // spread into a new list

Console.WriteLine(asArray.Length);
Console.WriteLine(asList.Count);
Console.WriteLine(string.Join(", ", combined));
Console.WriteLine(string.Join(", ", combined.GetRange(1, 3)));
Output
3
2
1, 2, 3, 4, 5, 6
2, 3, 4

Try it yourself

Build a simple to-do list: add tasks, mark one done by removing it, and print what's left in alphabetical order.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
4 tasks left:
  1. Buy milk
  2. Call mum
  3. Renew passport
  4. Water plants