Generic Lists (List<T>)
Reviewed & published by Brayan K
A single variable holds one value — but real programs juggle collections: a shopping cart, a list of scores, a feed of messages. List<T> is the workhorse collection of C#: an ordered, resizable container that grows as you add and shrinks as you remove. Master it and you can model almost any "many things" you'll meet.
Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Create and initialise a List<T>
- Add, Insert, Remove, RemoveAt and Clear items
- Access by index and loop with foreach
- Search with Contains, IndexOf, Find and FindAll
- Sort, Reverse and aggregate with Sum/Max/Average
- Remove items safely with RemoveAll
💡 Real-World Analogy
An array is a row of fixed, numbered parking bays — once the car park is built, you can't add a bay. A List<T> is a queue at a coffee shop: people join the back, someone leaves from the middle, the line stretches and contracts naturally, and everyone still has a clear position. That flexibility — adding and removing without rebuilding everything — is exactly why List is the default collection.
1. Creating & Reading a List
Declare a list by stating what it holds: List<string> for text, List<int> for numbers. Add items with Add, read one by its zero-based index with list[0] (or list[^1] for the last), and ask how many there are with Count. A foreach loop is the tidiest way to visit every element.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// List<string> = a growable, ordered collection of strings.
List<string> fruits = new List<string>();
fruits.Add("apple"); // grows automatically
fruits.Add("banana");
fruits.Add("cherry");
Console.WriteLine($"Count: {fruits.Count}"); // Count: 3
Console.WriteLine($"First: {fruits[0]}"); // First: apple
Console.WriteLine($"Last: {fruits[^1]}"); // Last: cherry (^1 = from end)
// You can initialise with values directly (collection initialiser):
List<int> scores = new List<int> { 90, 75, 88 };
Console.WriteLine($"Scores count: {scores.Count}"); // Scores count: 3
// Loop with foreach — the cleanest way to visit every item.
foreach (string fruit in fruits)
Console.WriteLine($"- {fruit}");
// - apple
// - banana
// - cherry
}
}
// ✅ Expected output:
// Count: 3
// First: apple
// Last: cherry
// Scores count: 3
// - apple
// - banana
// - cherry2. Modifying a List
Lists shine because they change shape. Insert places an item at a position, Remove deletes the first matching value, RemoveAt deletes by index, and Clear empties the whole thing. Contains and IndexOf answer "is it in here, and where?".
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string> { "wash", "cook", "sleep" };
tasks.Insert(1, "shop"); // put "shop" at index 1
// tasks = wash, shop, cook, sleep
tasks.Remove("cook"); // remove the first matching value
tasks.RemoveAt(0); // remove by index (drops "wash")
// tasks = shop, sleep
Console.WriteLine($"Remaining: {string.Join(", ", tasks)}");
// Remaining: shop, sleep
Console.WriteLine($"Has 'sleep'? {tasks.Contains("sleep")}"); // True
Console.WriteLine($"Index of 'sleep': {tasks.IndexOf("sleep")}"); // 1
tasks.Clear(); // empty the whole list
Console.WriteLine($"After clear: {tasks.Count}"); // After clear: 0
}
}
// ✅ Expected output:
// Remaining: shop, sleep
// Has 'sleep'? True
// Index of 'sleep': 1
// After clear: 0Your turn. Manage a small shopping cart — add one item, remove another, and print the result.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — manage a simple shopping list.
List<string> cart = new List<string> { "milk", "eggs" };
// 1) Add "bread" to the cart (hint: cart.Add(...))
___; // 👉 cart.Add("bread")
// 2) Remove "eggs" (hint: cart.Remove(...))
___; // 👉 cart.Remove("eggs")
// 3) Print how many items remain and list them
Console.WriteLine($"{cart.Count} items: {string.Join(", ", cart)}");
// ✅ Expected output: 2 items: milk, bread
}
}3. Sorting, Searching & Aggregating
Once you have a list of numbers, a whole toolbox opens up. Sort and Reverse reorder it in place; the LINQ helpers Sum, Max, Min and Average compute totals without a loop; and Find/FindAll pull out items matching a condition you express with a lambda (n => n > 4).
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> nums = new List<int> { 8, 3, 5, 1, 9, 2 };
// Sorting mutates the list in place.
nums.Sort();
Console.WriteLine(string.Join(", ", nums)); // 1, 2, 3, 5, 8, 9
nums.Reverse();
Console.WriteLine(string.Join(", ", nums)); // 9, 8, 5, 3, 2, 1
// Handy aggregate helpers from System.Linq:
Console.WriteLine($"Sum: {nums.Sum()}"); // Sum: 28
Console.WriteLine($"Max: {nums.Max()}"); // Max: 9
Console.WriteLine($"Min: {nums.Min()}"); // Min: 1
Console.WriteLine($"Avg: {nums.Average()}"); // Avg: 4.666...
// Find / filter without writing a loop:
int firstEven = nums.Find(n => n % 2 == 0); // 8
List<int> bigOnes = nums.FindAll(n => n > 4); // 9, 8, 5
Console.WriteLine($"First even: {firstEven}, big: {string.Join(",", bigOnes)}");
}
}
// ✅ Expected output:
// 1, 2, 3, 5, 8, 9
// 9, 8, 5, 3, 2, 1
// Sum: 28
// Max: 9
// Min: 1
// Avg: 4.666666666666667
// First even: 8, big: 9,8,5🔎 Deep Dive: the foreach removal trap
A classic bug: removing items while iterating a list with foreach. C# detects the collection changed underneath the loop and throws InvalidOperationException. The clean fix is RemoveAll with a predicate, which does the whole job safely in one call.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> readings = new List<int> { 12, -3, 7, -1, 20, 0 };
// ❌ Removing inside a foreach over the SAME list throws!
// foreach (int r in readings) if (r < 0) readings.Remove(r);
// ✅ RemoveAll takes a condition and removes every match safely.
int removed = readings.RemoveAll(r => r < 0);
Console.WriteLine($"Removed {removed} negatives."); // Removed 2 negatives.
Console.WriteLine(string.Join(", ", readings)); // 12, 7, 20, 0
}
}
// ✅ Expected output:
// Removed 2 negatives.
// 12, 7, 20, 0If you must remove inside a loop yourself, iterate backwards with a for loop (for (int i = list.Count - 1; i >= 0; i--)) so removing an item doesn't shift indexes you haven't visited yet.
🧩 Reorder Challenge
These six lines build a number list, drop the odd values, sort it, and print 2, 4, 6. Put them in a working order.
nums.Sort();
List<int> nums = new List<int> { 5, 2, 6, 1, 4, 3 };
Console.WriteLine(string.Join(", ", nums));
nums.RemoveAll(n => n % 2 != 0);
// (these four lines go inside Main)List<int> nums = new List<int> { 5, 2, 6, 1, 4, 3 };
nums.RemoveAll(n => n % 2 != 0);
nums.Sort();
Console.WriteLine(string.Join(", ", nums));Why: the list must exist before anything operates on it. Removing the odds first leaves 2, 6, 4, then Sort orders them to 2, 4, 6, and the WriteLine prints the final state. Sorting before removing would still work here, but removing first keeps the list smaller to sort.
🧠 Quick Recall
1. What does this print?
var list = new List<int> { 10, 20, 30 };
list.RemoveAt(0);
Console.WriteLine(list[0]);20 — RemoveAt(0) drops 10 and everything shifts down, so index 0 is now 20.
2. What is the output?
var l = new List<int> { 3, 1, 2 };
l.Sort();
Console.WriteLine(l[^1]);3 — after sorting, the list is 1, 2, 3, and [^1] is the last element, 3.
3. What prints here?
var l = new List<int> { 4, 8, 1 };
Console.WriteLine(l.Sum());13 — Sum() adds 4 + 8 + 1. (Requires using System.Linq;.)
Pro Tips
- 💡 Default to List<T> over an array whenever the size might change.
- 💡 Use RemoveAll(predicate) to delete every match — never remove inside a foreach.
- 💡 Index [^1] for the last item instead of list[list.Count - 1].
- 💡 Add using System.Linq; to unlock Sum, Max, Average, Where and friends.
Common Errors (and the fix)
- "ArgumentOutOfRangeException": you indexed past the end. A list of 3 has valid indexes 0–2; check Count first.
- "InvalidOperationException: collection was modified": you removed inside a foreach. Use RemoveAll or a backwards for.
- "The type or namespace 'List' could not be found": add using System.Collections.Generic;.
- Remove "did nothing": Remove deletes the first matching value; if it's absent it returns false and changes nothing.
Mini-Challenge: Scores Report
Combine adding, filtering with RemoveAll, sorting, and aggregating into a small scores report. The outline is in the comments.
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// 🎯 MINI-CHALLENGE: Exam scores report
// 1. Start with: List<int> scores = new List<int> { 55, 90, 73, 40, 88, 67 };
// 2. Add one more score: 100.
// 3. Remove every failing score below 50 (hint: RemoveAll).
// 4. Sort the remaining scores.
// 5. Print: count, the sorted list, the highest, and the average (1 dp).
//
// ✅ Example output:
// 6 passing scores: 55, 67, 73, 88, 90, 100
// Highest: 100
// Average: 78.8
// your code here
}
}🎉 Lesson Complete
- ✅ List<T> is an ordered, resizable collection — the default for "many things"
- ✅ Add, Insert, Remove, RemoveAt, Clear change its contents
- ✅ Index with [i] (and [^1] for the last); count with Count
- ✅ Sort, Reverse, and LINQ Sum/Max/Average do the heavy lifting
- ✅ Use RemoveAll — never remove inside a foreach
Practice quiz
What is the main difference between an array and a List<T> in C#?
- A List is always faster than an array
- A List grows and shrinks automatically, while an array has a fixed size
- An array can hold mixed types but a List cannot
- A List can only hold reference types
Answer: A List grows and shrinks automatically, while an array has a fixed size. An array's size is fixed at creation; a List<T> resizes automatically as you Add and Remove, which is why it is the default collection.
What does the <T> in List<T> represent?
- The total number of items
- A type parameter — the kind of element the list holds
- A threading mode
- A temporary variable
Answer: A type parameter — the kind of element the list holds. T is a type parameter you fill in when creating the list, so List<int> holds ints and List<string> holds strings, type-checked at compile time.
What does list[^1] return?
- The first element
- The element at index 1
- The last element (one from the end)
- The count of the list
Answer: The last element (one from the end). The ^1 index counts from the end, so list[^1] is the last element — cleaner than list[list.Count - 1].
What is the correct, safe way to remove every item matching a condition?
- Remove items inside a foreach over the same list
- Use RemoveAll with a predicate
- Call Clear() then re-add the keepers
- Set each matching item to null
Answer: Use RemoveAll with a predicate. RemoveAll(predicate) removes every match in one call. Removing inside a foreach over the same list throws InvalidOperationException.
What happens if you modify a List inside a foreach loop over that same list?
- It silently skips the modified item
- It throws InvalidOperationException
- It sorts the list automatically
- Nothing — it is fully supported
Answer: It throws InvalidOperationException. C# detects the collection changed underneath the loop and throws InvalidOperationException. Use RemoveAll or a backward for loop instead.
What does RemoveAt(0) do to a list of { 10, 20, 30 }?
- Removes 10, leaving { 20, 30 }
- Removes 0 items
- Removes the last item
- Removes the value 0
Answer: Removes 10, leaving { 20, 30 }. RemoveAt(0) removes the item at index 0 (the value 10); everything shifts down, so index 0 becomes 20.
Which using directive must be imported to call list.Sum(), Max(), and Average()?
- System.Text
- System.Collections.Generic
- System.Linq
- System.IO
Answer: System.Linq. Sum, Max, Min, and Average are LINQ extension methods that live in System.Linq.
What does the Sort() method do to a List<int>?
- Returns a new sorted list and leaves the original unchanged
- Sorts the list in place, mutating it
- Removes duplicates
- Reverses the list
Answer: Sorts the list in place, mutating it. Sort() mutates the list in place, reordering its elements; it does not return a new list.
What does Remove("x") do if "x" is not in the list?
- Throws an exception
- Removes the first item instead
- Returns false and changes nothing
- Adds "x" to the list
Answer: Returns false and changes nothing. Remove deletes the first matching value and returns true; if the value is absent it returns false and the list is unchanged.
Which namespace must you import to declare a List<T>?
- System.Linq
- System.Collections.Generic
- System.Text
- System
Answer: System.Collections.Generic. List<T> lives in System.Collections.Generic, so that namespace must be imported (most editors add it automatically).
Continue this course
- Previous: Strings, Interpolation & StringBuilder
- Next: Dictionary (Key-Value Pairs) — Fast key-value lookups with Dictionary
- Quick reference: C# cheat sheet › Collections
Frequently asked questions
What is the difference between an array and a List in C#?
An array has a fixed size set when you create it — you cannot add or remove elements. A List grows and shrinks automatically as you Add and Remove items, which is why it is the default choice for collections whose size changes. Arrays are slightly faster and lighter, so they suit fixed-size data, but most everyday code reaches for List.
What does the <T> in List<T> mean?
T is a type parameter — a placeholder you fill in when you create the list. A List of int holds integers, a List of string holds text, and a List of a custom Product class holds products. Because the type is fixed at compile time, the compiler stops you putting the wrong kind of item in, and you get no boxing for value types.
How do I remove items from a List while looping over it?
Never modify a List inside a foreach over that same List — it throws InvalidOperationException. Instead use RemoveAll with a condition, loop backwards with a for loop using indexes, or build a new filtered list. RemoveAll(x => x < 0) is the cleanest way to drop every item matching a rule.
Is List<T> the same as ArrayList?
No, and you should prefer List<T>. ArrayList is the old non-generic collection that stores everything as object, which means boxing for value types and runtime cast errors. List<T> is type-safe and faster. ArrayList only survives in very old code; new code should always use the generic List.