← Open in the full interactive course (progress tracking, search & more)

List<T> is an array that grows for you — the same indexed, ordered collection, minus the "fixed size" problem.

Think about a shopping cart on an e-commerce site. You don't know in advance how many items a customer will add — it might be 1, it might be 40. An array can't help here: its size is locked in the moment you create it. You'd need to guess a size, and guessing wrong means either wasted space or a cart that silently can't hold any more items.

In this lesson, you'll learn about List<T> — the resizable, general-purpose collection you'll reach for constantly in real C# code — how it grows internally, and how it compares to the array you already know.

What Is It?

The Simple Explanation

A List<T> is like an array that can grow and shrink while your program runs. You add items with .Add(), remove them with .Remove(), and the list quietly handles resizing itself behind the scenes. You never have to guess a size up front.

The Technical Definition

List<T> is a generic, resizable, ordered collection class in the System.Collections.Generic namespace. The T is a placeholder for whatever type you're storing — List<int>, List<string>, List<Product>. It preserves insertion order, allows duplicate values, supports index-based access like an array, and automatically manages its own capacity as you add or remove elements.

List<T> = Array + Automatic Resizing
List<string> cart = ["Book", "Pen"]; // collection expression — clean, modern cart.Add("Notebook"); // grows automatically — now has 3 items

Why Does It Exist?

The Problem

Recall from the arrays lesson: once created, an array's size is permanent. If you don't know how many elements you'll need — which is true for the vast majority of real-world data — you're stuck. You could try to work around it manually:

int[] cart = new int[10]; // guess: "10 items should be enough" int count = 0; void AddItem(int productId) { if (count == cart.Length) { // Array is full! Now what? Manually create a bigger array, // copy every old element into it, then add the new one... int[] bigger = new int[cart.Length * 2]; Array.Copy(cart, bigger, cart.Length); cart = bigger; } cart[count] = productId; count++; }

This works, but it's tedious, error-prone boilerplate that every program needing a growable collection would have to reinvent — and get subtly wrong.

The Solution

List<T> does exactly that "create a bigger array, copy everything over" dance for you, automatically, every time it runs out of room:

List<int> cart = []; cart.Add(101); // just works, no matter how many times you call it

Big Picture

ARRAY vs LIST<T>
Array
Fixed size at birth
No Add / Remove
Compact & fast
List<T>
Grows and shrinks as needed
Wraps an array internally
Flexible & convenient
List<T> is not a replacement for arrays — it's built on top of one, managed for you.

How It Works

USING LIST<T>, STEP BY STEP
1. CREATE A LIST
List<string> names = [];              // empty list
List<string> names2 = ["Ana", "Ben"];  // pre-filled list
2. ADD, INSERT, AND REMOVE
names.Add("Cara");            // adds to the end
names.Insert(0, "Aaron");     // inserts at a specific position
names.Remove("Ben");          // removes the first match by value
names.RemoveAt(0);            // removes by index
3. READ BY INDEX, CHECK COUNT
string first = names[0];
Console.WriteLine(names.Count);   // note: Count, not Length
4. ITERATE, SEARCH, SORT
foreach (string name in names)
    Console.WriteLine(name);

bool hasCara = names.Contains("Cara");
names.Sort();

Simple Example

List<int> scores = []; scores.Add(95); scores.Add(82); scores.Add(71); Console.WriteLine($"Count: {scores.Count}"); // 3 Console.WriteLine($"First: {scores[0]}"); // 95 scores.Remove(82); Console.WriteLine($"After remove: {string.Join(", ", scores)}"); // 95, 71 Console.WriteLine($"Count: {scores.Count}"); // 2 — the list shrank

Code → Meaning → Result:

Real-World Example

An e-commerce shopping cart is the canonical use case for List<T> — items are added and removed constantly as the customer shops, and the total is never known ahead of time.

public record CartItem(string Name, decimal Price, int Quantity); public class ShoppingCart { private readonly List<CartItem> _items = []; public IReadOnlyList<CartItem> Items => _items; public void AddItem(CartItem item) => _items.Add(item); public bool RemoveItem(string name) { var match = _items.FirstOrDefault(i => i.Name == name); return match is not null && _items.Remove(match); } public decimal Total() { decimal total = 0; foreach (var item in _items) total += item.Price * item.Quantity; return total; } } var cart = new ShoppingCart(); cart.AddItem(new CartItem("Book", 19.99m, 2)); cart.AddItem(new CartItem("Pen", 2.50m, 3)); cart.AddItem(new CartItem("Notebook", 4.75m, 1)); Console.WriteLine($"Items in cart: {cart.Items.Count}"); // 3 Console.WriteLine($"Total: {cart.Total():C}"); // $52.23 cart.RemoveItem("Pen"); Console.WriteLine($"Items after removal: {cart.Items.Count}"); // 2

Notice the cart exposes Items as IReadOnlyList<CartItem> rather than the raw List<CartItem> — outside code can look at the cart's contents, but can only change them through AddItem/RemoveItem. You'll see this exact interface, and several like it, formalized in the "Collection Interfaces" lesson later in this module.

Under the Hood

HOW LIST<T> ACTUALLY GROWS
1. A LIST<T> WRAPS AN ARRAY
2. WHAT HAPPENS WHEN CAPACITY RUNS OUT
3. WHY THIS MAKES "ADD AT THE END" CHEAP ON AVERAGE
4. WHY INSERT/REMOVE IN THE MIDDLE ARE SLOWER

Common Confusion

1. .Length vs .Count

Arrays expose .Length. List<T> exposes .Count. There is no deep technical reason they differ — it's simply a naming inconsistency baked into the .NET class library decades ago, and every C# developer just learns to remember it.

2. "Count" (the property) vs "Count()" (a later LINQ method)

Later in this course you'll meet LINQ's Count() method, which works on almost any collection but has to actually iterate to count. On List<T>, always prefer the Count property shown in this lesson — it's a simple stored number, instant to read, with no iteration involved.

3. Capacity vs Count

Count is how many elements are actually in the list right now. Capacity is how many slots the backing array currently has reserved — often more than Count, to leave room to grow without reallocating on every single addition.

Common Mistakes

Mistake 1 — Using .Length on a List

Wrong — compiler error, List<T> has no Length:

List<int> scores = [95, 82]; Console.WriteLine(scores.Length); // compiler error

Correct:

Console.WriteLine(scores.Count); // 2

Mistake 2 — Modifying a list while iterating over it with foreach

Wrong — throws InvalidOperationException ("Collection was modified"):

foreach (int score in scores) { if (score < 80) scores.Remove(score); // mutating the list mid-iteration }

Correct — iterate over a snapshot copy, or loop backwards by index:

for (int i = scores.Count - 1; i >= 0; i--) { if (scores[i] < 80) scores.RemoveAt(i); // safe — going backwards avoids skipping elements }

Mistake 3 — Indexing past the end

Just like arrays, list[list.Count] throws ArgumentOutOfRangeException — the last valid index is always Count - 1, the same off-by-one rule you learned with arrays.

Mistake 4 — Reaching for List<T> when you never intend to change it

If a collection is genuinely fixed and never grows or shrinks, List<T> adds a small amount of unnecessary overhead compared to a plain array. It's rarely a real problem, but for fixed, performance-sensitive data, an array can be the better fit.

When Should I Use It?

Use List<T> when

Reach for something else when

Rule of thumb: When you're not sure which collection to use, start with List<T>. It's the most common, most flexible, general-purpose collection in .NET — you can always switch to something more specialized once you know your access pattern.

Mental Model

List<T> = a self-managing array
Add() = "grow the array if you have to, then put this at the end"
Count = how many items are really there right now

Remember:
· Under the hood, it's still an array — with automatic resizing bolted on.
· Count, not Length — a naming quirk worth memorizing.
· Adding to the end is cheap; inserting/removing in the middle shifts everything after it.

Key Takeaway


Check Your Understanding

You've seen how List<T> gives you a resizable, array-like collection. Let's check your understanding.

1. What is the fundamental difference between an array and a List<T>?

Show answer

Correct: B

Why B is correct: This is the defining distinction. An array's length is locked in when it's created. List<T> is built specifically to add and remove elements freely, resizing its internal backing array as needed.

Why A is incorrect: List<T> is generic — it can hold any type, exactly like an array can.

Why C is incorrect: Both arrays and lists support foreach just fine.

Why D is incorrect: They behave very differently once you need to add or remove elements — that's precisely the situation where the choice matters.

Reinforcement: Resizability is the single biggest reason to choose List<T> over a plain array.

2. What happens internally when you call Add on a List<T> whose backing array is already full?

Show answer

Correct: C

Why C is correct: When capacity runs out, List<T> allocates a new, larger array (typically double the size), copies every existing element over, and only then adds the new value — all automatically, without you having to write that logic yourself.

Why A is incorrect: Unlike a fixed array, a full List<T> is never "stuck" — it simply grows.

Why B is incorrect: The new item is always added; nothing is silently dropped.

Why D is incorrect: List<T> doesn't evict old items to make room — it grows its capacity instead. (A fixed-capacity, oldest-evicted structure is a different concept entirely.)

Reinforcement: The "resize and copy" work you'd have to do manually with an array is exactly what List<T> automates for you.

3. Why is list.RemoveAt(0) generally more expensive than list.Add(value) (appending to the end)?

Show answer

Correct: B

Why B is correct: The backing array must stay contiguous with no gaps. Removing the first element means every other element has to shift left by one slot. Adding to the end, by contrast, usually just drops the value into the next free slot — no shifting needed.

Why A is incorrect: RemoveAt is a real, commonly used List<T> method.

Why C is incorrect: Most Add calls don't trigger a resize at all — only the occasional call that exceeds current capacity does.

Why D is incorrect: Their cost differs specifically because of where in the list the operation happens — front operations are pricier than end operations.

Reinforcement: Operations at the end of a List<T> are cheap; operations at the front or middle require shifting elements to preserve contiguous storage.

4. An e-commerce team is choosing between an array and a List<T> to represent a shopping cart's items, where the customer adds and removes products while browsing. Which is the better fit, and why?

Show answer

Correct: B

Why B is correct: A shopping cart's item count is inherently unpredictable and changes constantly — exactly the scenario List<T> was built for. An array would force a hard, arbitrary size limit chosen in advance.

Why A is incorrect: Arrays aren't universally faster — they're only a good fit when the size is genuinely fixed, which a shopping cart is not.

Why C is incorrect: A cart needs to track multiple distinct items with their own details (name, price, quantity), not a single number.

Why D is incorrect: Real shopping carts routinely hold many items — assuming otherwise would make the design fragile from day one.

Reinforcement: When the number of elements changes at runtime, List<T> is almost always the right default choice.

You now have the most-used collection in everyday C# code in your toolkit. Next up: fast lookups by key with Dictionary<TKey, TValue>.


dotnetmadeeasy.com — Learn C# and .NET, the right way.