Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 5 – Arrays and collections

Lists and collection expressions

How a C# List<T> grows by doubling, what Add, Insert and Remove cost, how collection expressions build lists, arrays and spans, and how to share a list safely.

  • Beginner
  • 20 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A list keeps its items in an array that doubles when full · Adding at the end is cheap; inserting at the front moves every item · Collection expressions build lists, arrays and spans with one syntax. With a pass: 3 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Add, insert, remove and search List<T> items and predict what each operation costs
  • Measure how Capacity grows, and size a list up front when the count is known
  • Build arrays, lists and spans with collection expressions and the spread element
  • Choose how a class hands out its list so that callers cannot change it

Before you start

A waiting list that grows to 1,000 bookings one Add at a time makes only 9 arrays and copies 1,020 items along the way, about one copy per booking. Inserting each booking at the front instead was about 300 times slower than adding at the end and reversing once.

You will see how a List<T> grows, what each of its operations costs, how collection expressions build lists, arrays and spans in one syntax, and how to hand a list to other code without handing over control of it.

A list keeps its items in an array that doubles when full

A List<T> is an array with spare room. Count is the number of items in use and Capacity the length of the array inside. While there is room, Add writes the next slot. When Count equals Capacity, the reference documentation says the list reallocates its internal array and copies the existing elements before adding. This program adds bookings one by one and prints each change of Capacity, then counts what 1,000 Adds cost:

growth.cs: Count, Capacity and the copies of 1,000 Adds C# · growth/growth.cs
// A waiting list that grows one booking at a time. Capacity is the length of the array inside the list.
var waiting = new List<int>();
Console.WriteLine($"new List<int>()  Count {waiting.Count,2}, Capacity {waiting.Capacity,2}");
int capacity = waiting.Capacity;
for (int pnr = 1; pnr <= 17; pnr++)
{
    int before = waiting.Count;
    waiting.Add(pnr);
    if (waiting.Capacity != capacity)
    {
        Console.WriteLine($"Add #{pnr,-2}          Count {waiting.Count,2}, Capacity {waiting.Capacity,2}: a new array of {waiting.Capacity}, {before} items copied into it");
        capacity = waiting.Capacity;
    }
}

// 1,000 Adds without a capacity, then with one: arrays made, items copied, bytes allocated.
long start = GC.GetAllocatedBytesForCurrentThread();
var grown = new List<int>();
int arrays = 0;
int copied = 0;
for (int i = 0; i < 1_000; i++)
{
    if (grown.Count == grown.Capacity)
    {
        arrays++;
        copied += grown.Count;
    }
    grown.Add(i);
}
long grownBytes = GC.GetAllocatedBytesForCurrentThread() - start;

start = GC.GetAllocatedBytesForCurrentThread();
var sized = new List<int>(1_000);
for (int i = 0; i < 1_000; i++)
{
    sized.Add(i);
}
long sizedBytes = GC.GetAllocatedBytesForCurrentThread() - start;

Console.WriteLine($"1,000 Adds to new List<int>()      {arrays} arrays, {copied} items copied, {grownBytes} bytes, Capacity {grown.Capacity}");
Console.WriteLine($"1,000 Adds to new List<int>(1000)  1 array, 0 items copied, {sizedBytes} bytes, Capacity {sized.Capacity}");
grown.TrimExcess();
Console.WriteLine($"TrimExcess()                       Capacity {grown.Capacity}");

Output

new List<int>()  Count  0, Capacity  0
Add #1           Count  1, Capacity  4: a new array of 4, 0 items copied into it
Add #5           Count  5, Capacity  8: a new array of 8, 4 items copied into it
Add #9           Count  9, Capacity 16: a new array of 16, 8 items copied into it
Add #17          Count 17, Capacity 32: a new array of 32, 16 items copied into it
1,000 Adds to new List<int>()      9 arrays, 1020 items copied, 8424 bytes, Capacity 1024
1,000 Adds to new List<int>(1000)  1 array, 0 items copied, 4056 bytes, Capacity 1000
TrimExcess()                       Capacity 1024

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run growth.cs

Step through one full array, using the run’s numbers:

Interactive · stage by stage What one Add does when the list is full Step through the Adds of growth.cs and watch Count, Capacity and the items copied.
What one Add does when the list is full, step 7 of 7. Showing new List<int>(), Adds 1 to 4 (Capacity 16, then 32), Add #5: no room (Count 4 = Capacity 4), A new array of 8 (Capacity 8), Copy (4 items copied). In focus: Copy.room againnew List<int>()Count 0, Capacity 0Adds 1 to 4Capacity 16, then 32Add #5: no roomCount 4 = Capacity 4A new array of 8Capacity 8Copy4 items copied

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. An empty list. A new List<int>() has no array yet: Count 0 and Capacity 0. Capacity is the length of the array inside the list; Count is how many items are in use.
  2. The first Add. Add #1 finds no room, so the list makes an array of 4 and stores the item. Adds 2, 3 and 4 write into the spare room: nothing is allocated or copied.
  3. Full. Add #5 finds Count equal to Capacity. The list cannot make its array longer, any more than an array can grow.
  4. Twice the room. It allocates a new array of 8, twice the old capacity. The run printed this step as Capacity 8 at Add #5.
  5. Copy, then store. The 4 items are copied into the new array, item 5 goes after them, and the old array is left for the garbage collector. Count is 5.
  6. The pattern repeats. Adds 6 to 8 use the spare room again. Add #9 copies 8 items into an array of 16, and Add #17 copies 16 into one of 32, as the run printed.
  7. A thousand Adds. For 1,000 Adds the list made 9 arrays, from 4 to 1,024, and copied 1,020 items in all: about one copy per Add. A list made with new List<int>(1000) made 1 array and copied nothing.

The capacity went 4, 8, 16, 32: each new array is twice the last, so copies get rarer as the list grows. That is why Add is amortised cost: The average cost of an operation over a long run of them. List<T>.Add sometimes copies every item into a larger array, but because the array doubles, the copies average out to about one per Add, so Add is amortised O(1). O(1): an occasional Add copies everything, but the average stays about one copy per item. When you know the count, pass it: new List<int>(1000) made one array and allocated 4,056 bytes instead of 8,424. TrimExcess() left Capacity at 1,024, because it trims only when more than 10 % is unused.

Checkpoint

Check: How a list grows

  1. Question 1 of 2 A new List<int>() gets 9 Adds. What is its Capacity after the ninth?

    Type a number.

    A hint

    The first Add makes room for 4; each time the array is full, the next Add doubles it.

    Show the answer: Question 1 of 2

    Answer: 16 items

    Add #1 makes an array of 4, Add #5 one of 8, and Add #9 one of 16, exactly as growth.cs printed: Count 9, Capacity 16.

  2. Question 2 of 2 You will add exactly 1,000 items to a list. What does new List<int>(1000) save compared with new List<int>()?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The 8 extra arrays and the 1,020 copies; it allocated 4,056 bytes instead of 8,424

    Passing the expected count to the constructor allocates the right array once, which the documentation recommends to avoid the cost of reallocations.

Adding at the end is cheap; inserting at the front moves every item

An array keeps its items side by side, so a list can add at the end without touching the others, while inserting at index 0 must move every item one place up. The documentation gives each operation its cost, where n is Count:

Operation Cost Why
list[i], read or write O(1) goes straight to slot i
Add(x) O(1) amortised writes the next slot; O(n) when it must grow
Insert(i, x) O(n) moves the items from i on one place up
RemoveAt(i) O(n − i) moves the items after i one place down
Contains(x), IndexOf(x), Remove(x) O(n) compares with each item in turn

This program puts 20,000 bookings in newest-first order three ways and times each:

costs.cs: newest first, three ways, timed C# · costs/costs.cs
using System.Diagnostics;
using System.Globalization;

// 20,000 bookings put into a list three ways; each way runs 5 times and the fastest run counts.
const int N = 20_000;
CultureInfo inv = CultureInfo.InvariantCulture;
int[] expected = [.. Enumerable.Range(0, N).Reverse()];   // newest booking first

Report("Insert(0, x) for each", () =>
{
    var list = new List<int>();
    for (int i = 0; i < N; i++)
    {
        list.Insert(0, i);   // moves every item already in the list one place up
    }
    return list;
});

Report("Add(x), then Reverse()", () =>
{
    var list = new List<int>();
    for (int i = 0; i < N; i++)
    {
        list.Add(i);         // writes at the end: nothing moves
    }
    list.Reverse();          // one pass over the list
    return list;
});

Report("RemoveAt(0) until empty", () =>
{
    var list = new List<int>(expected);
    var taken = new List<int>(N);
    while (list.Count > 0)
    {
        taken.Add(list[0]);
        list.RemoveAt(0);    // moves every item after index 0 one place down
    }
    return taken;
});

void Report(string name, Func<List<int>> build)
{
    if (!build().SequenceEqual(expected))
    {
        throw new InvalidOperationException($"{name} gave a different order");
    }
    double best = double.MaxValue;
    for (int run = 0; run < 5; run++)
    {
        var clock = Stopwatch.StartNew();
        build();
        best = Math.Min(best, clock.Elapsed.TotalMilliseconds);
    }
    Console.WriteLine($"{name,-26}{best.ToString("F2", inv),9} ms");
}

Output

Insert(0, x) for each         17.85 ms
Add(x), then Reverse()         0.06 ms
RemoveAt(0) until empty       17.88 ms

This output changes from run to run: the times depend on the computer and on what else it is doing

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run costs.cs

On this run Insert(0, x) took 17.85 ms and Add then Reverse() 0.06 ms, about 300 times faster for the same order, and taking items from the front with RemoveAt(0) was as slow as inserting there. Twice the bookings would make the front-insert version about four times slower, because insert k moves k items.

Using a List<T> as a queue

list.RemoveAt(0) to take the oldest item moves every other item down, so a busy queue in a list does work that grows with the square of its length. A Queue<T> takes from the front in O(1); a later lesson of this module covers it.

Checkpoint

Check: What each list operation costs

  1. One question 20,000 Insert(0, x) calls took 17.85 ms. About how long would 40,000 take on the same computer?

    Choose one answer.

    Show the answer: One question

    Answer: About 70 ms, four times as long

    Inserting at the front of a list of k items moves k items, so n inserts move about n²/2 in all. Doubling n roughly quadruples the time; build in another order and reverse once instead.

Collection expressions build lists, arrays and spans with one syntax

Square brackets build a collection: [6, 7, 8] is a collection expression (C# 12), and a spread element: An element written .. collection inside a collection expression, such as [.. morning, 12]. It puts every element of the collection in its place, in order; the collection can be anything a foreach loop can read., .. morning, puts all the elements of another collection in its place. The type you assign it to decides what is built:

expressions.cs: one syntax for lists, arrays, spans, sets and interfaces C# · expressions/expressions.cs
using System.Collections.Immutable;

// Departure hours of a day's trains, built with collection expressions.
int[] morning = [6, 7, 8];
int[] evening = [17, 18, 19];

List<int> departures = [.. morning, 12, .. evening];   // spread both arrays around one more hour
Console.WriteLine($"List<int>            [{string.Join(", ", departures)}], Count {departures.Count}, Capacity {departures.Capacity}");

// The same syntax for other targets; the type on the left decides what is built.
int[] copy = [.. departures];
Span<int> firstThree = [6, 7, 8];
ImmutableArray<int> fixedHours = [.. departures];
HashSet<string> codes = ["NDLS", "MAS", "NDLS"];
Console.WriteLine($"int[]                Length {copy.Length}, a different object: {!ReferenceEquals(copy, morning)}");
Console.WriteLine($"Span<int>            Length {firstThree.Length}");
Console.WriteLine($"ImmutableArray<int>  Length {fixedHours.Length}");
Console.WriteLine($"HashSet<string>      Count {codes.Count}: the second NDLS was not added");

// Interfaces: the compiler picks a type that can be changed only when the interface allows changes.
IList<int> canChange = [1, 2, 3];
IReadOnlyList<int> cannotChange = [1, 2, 3];
Console.WriteLine($"IList<int>           a {canChange.GetType().Name}, IsReadOnly {canChange.IsReadOnly}");
Console.WriteLine($"IReadOnlyList<int>   IsReadOnly {((ICollection<int>)cannotChange).IsReadOnly}");

int[] none = [];
Console.WriteLine($"int[] none = []      Length {none.Length}, the shared Array.Empty<int>(): {ReferenceEquals(none, Array.Empty<int>())}");

Output

List<int>            [6, 7, 8, 12, 17, 18, 19], Count 7, Capacity 7
int[]                Length 7, a different object: True
Span<int>            Length 3
ImmutableArray<int>  Length 7
HashSet<string>      Count 2: the second NDLS was not added
IList<int>           a List`1, IsReadOnly False
IReadOnlyList<int>   IsReadOnly True
int[] none = []      Length 0, the shared Array.Empty<int>(): True

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run expressions.cs

Step through the first line, the departures list:

Interactive · stage by stage How [.. morning, 12, .. evening] becomes a list Step through the departures list of expressions.cs.
How [.. morning, 12, .. evening] becomes a list, step 5 of 5. Showing The target type, Count the elements (3 + 1 + 3 = 7), Make the list (Capacity 7), Fill it in order (6, 7, 8, 12, 17, 18, 19).The target typeList<int> departuresCount the elements3 + 1 + 3 = 7Make the listCapacity 7Fill it in order6, 7, 8, 12, 17, 18, 19

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The type on the left decides. A collection expression has no type of its own. Here the variable is a List<int>, so the compiler builds a List<int>; the same brackets would build an int[] for an int[] variable.
  2. Count first. morning and evening are arrays, so their lengths are known before anything is copied: 3 + 1 + 3 = 7 elements.
  3. Room for 7. The list is made with room for exactly 7, which is why the run printed Capacity 7. Seven Adds to an empty list would have made two arrays, of 4 and then 8, and ended at Capacity 8.
  4. Spread, item, spread. The elements go in the order they are written: 6, 7 and 8 from morning, then 12, then 17, 18 and 19 from evening. Nothing is copied twice.
  5. Done. departures holds 7 hours in a list of capacity 7. Changing morning later does not change it: the expression copied the elements when it ran.

The list came out with Capacity 7, not the 8 that seven Adds would leave: the compiler counted the elements first. The same brackets built an int[], a Span<int>, an ImmutableArray<int> and a HashSet<string>, which kept one NDLS. Collection expressions work for interfaces too: an IList<int> got a List<int> that can change, and an IReadOnlyList<int> a type whose IsReadOnly is true. [] gave the shared empty array, which costs nothing.

Key idea

A list grows by doubling, so Add at the end is cheap and anything at the front moves every item; collection expressions build lists, arrays and spans alike, sized to their elements.

Checkpoint

Check: Collection expressions and spreads

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    int[] a = [1, 2];
    int[] b = [5];
    List<int> all = [0, .. a, .. b, 9];
    Console.WriteLine(string.Join(" ", all));
    Show the answer: Question 1 of 2

    Answer: 0 1 2 5 9

    [0, .. a, .. b, 9] builds a List<int> of the five ints in order, as [.. morning, 12, .. evening] built the departures list.

  2. Question 2 of 2 What does IList<int> hours = [6, 7, 8]; create?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A List<int> that can still be changed

    The target type decides what a collection expression builds. An interface that allows changes gets a List<T>, and a read-only interface gets a type that cannot be changed.

Removing many items without quadratic time, measured Pro: opens with a Pro, Premium or Ultimate pass

Removing every cancelled booking from a list looks like a three-line loop, and the obvious loop is both wrong and slow. The run below removes 10,000 of 20,000 bookings four ways and counts what each one leaves behind.

In this part:

  • Four ways to remove cancelled bookings, timed and counted
  • Inserting at the front, and what to use instead

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Sharing a list without handing over control Pro: opens with a Pro, Premium or Ultimate pass

A class that hands out its own List<T> hands out the power to change its data. The run below exposes a coach’s passenger list four ways, shows what a caller can do with each, and counts what each costs per call.

In this part:

  • Four ways to expose a list, and what a caller can do with each
  • A span over a list goes stale when the list grows

2 runnable examples (C#) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Sorting a list: unstable Sort and stable OrderBy, measured Pro: opens with a Pro, Premium or Ultimate pass

Two sorts of the same bookings can disagree about the order of equal keys, and the difference can reach a report or a test. The run below counts how often List<T>.Sort reorders equal keys, and what the stable alternative allocates.

In this part:

  • Equal keys come out in any order with List.Sort
  • Two keys, or a stable sort, and what each allocates

1 runnable example (C#) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: a playlist that never throws

playlist.cs checks a Playlist class that you finish. It keeps its songs in a private list and moves or removes them by index, returning false for a move or a removal that is not possible. Only the first check passes until the three methods are written:

playlist.cs: eight checks of the Playlist class C# · playlist/playlist.cs
// Practice: finish the Playlist class so that every check below passes, then run this file again.
// MoveUp and MoveDown swap a song with its neighbour; Remove takes a song out by its index.
// A move or a remove that is not possible returns false and leaves the playlist as it was.
var checks = new List<(string Name, Func<Playlist, bool> Act, bool Returns, string After)>
{
    ("Add three songs", p => true, true, "Raga, Tala, Dhun"),
    ("MoveUp(1)", p => p.MoveUp(1), true, "Tala, Raga, Dhun"),
    ("MoveUp(0) at the top", p => p.MoveUp(0), false, "Raga, Tala, Dhun"),
    ("MoveDown(2) at the end", p => p.MoveDown(2), false, "Raga, Tala, Dhun"),
    ("MoveDown(0)", p => p.MoveDown(0), true, "Tala, Raga, Dhun"),
    ("Remove(1)", p => p.Remove(1), true, "Raga, Dhun"),
    ("Remove(3) past the end", p => p.Remove(3), false, "Raga, Tala, Dhun"),
    ("MoveUp(-1)", p => p.MoveUp(-1), false, "Raga, Tala, Dhun"),
};

int passed = 0;
foreach (var (name, act, returns, after) in checks)
{
    string verdict;
    try
    {
        var playlist = new Playlist();
        playlist.Add("Raga");
        playlist.Add("Tala");
        playlist.Add("Dhun");
        bool got = act(playlist);
        string songs = string.Join(", ", playlist.Songs);
        verdict = got == returns && songs == after ? "ok" : $"FAIL, returned {got} with {songs}";
    }
    catch (Exception e)
    {
        verdict = $"FAIL, {e.GetType().Name}";
    }
    if (verdict == "ok")
    {
        passed++;
    }
    Console.WriteLine($"{name}: {verdict}");
}
Console.WriteLine($"{passed} of {checks.Count} checks pass");
return passed == checks.Count ? 0 : 1;

sealed class Playlist
{
    private readonly List<string> _songs = [];

    // Callers can read the songs but not change them.
    public IReadOnlyList<string> Songs => _songs.AsReadOnly();

    public void Add(string song) => _songs.Add(song);

    public bool MoveUp(int index) => throw new NotImplementedException("Check index, then swap _songs[index] and _songs[index - 1].");

    public bool MoveDown(int index) => throw new NotImplementedException("Check index, then swap with the song after it.");

    public bool Remove(int index) => throw new NotImplementedException("Check index, then RemoveAt.");
}

Output (exit status 1)

Add three songs: ok
MoveUp(1): FAIL, NotImplementedException
MoveUp(0) at the top: FAIL, NotImplementedException
MoveDown(2) at the end: FAIL, NotImplementedException
MoveDown(0): FAIL, NotImplementedException
Remove(1): FAIL, NotImplementedException
Remove(3) past the end: FAIL, NotImplementedException
MoveUp(-1): FAIL, NotImplementedException
1 of 8 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run playlist.cs

Finish the playlist

Copy playlist.cs and write MoveUp, MoveDown and Remove. Check the index against _songs.Count first, and swap two songs with (_songs[i], _songs[i - 1]) = (_songs[i - 1], _songs[i]). Run the file with dotnet run playlist.cs until it prints 8 of 8 checks pass.

The questions below go back over capacity, the cost table and collection expressions, the weakest idea first:

Practice that adapts to you

Practise this lesson

7 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 What does the Capacity of a List<T> measure?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: The length of the array inside the list

    Count is how many items are in use and Capacity how many fit in the current array. growth.cs printed both after every change of capacity.

  2. Question 2 of 7 1,000 Adds to a new List<int>() copied 1,020 items. About how many items do 2,000 Adds copy?

    Type a number.

    A hint

    List the capacities the list passes through on the way to 2,000, and add up what each growth copies.

    Show the answer: Question 2 of 7

    Answer: 2044 items (anything from 1984 to 2104 counts)

    The array goes 4, 8, 16 … 2,048, and each growth copies the items of the full array: 4 + 8 + … + 1,024 = 2,044. Twice the Adds, about twice the copies: the cost per Add stays about one copy, which is what amortised O(1) means.

  3. Question 3 of 7 Which List<T> operation is O(1), whatever the size of the list?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: Reading list[i]

    Indexing is constant time; Add is O(1) amortised; Insert, RemoveAt near the front, Contains, IndexOf and Remove grow with the number of items.

  4. Question 4 of 7 A worker takes jobs from the front of a List<Job> with list[0] and list.RemoveAt(0) and adds new jobs with Add. With 100,000 waiting jobs it slows to a crawl. Why?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: Each RemoveAt(0) moves every remaining job down one place, so taking all of them costs O(n²)

    Taking from the front of a list is the expensive end. A Queue<T> takes from the front in O(1), which is the structure this job needs.

  5. Question 5 of 7 What did int[] none = []; give in the run?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: The one shared empty array, Array.Empty<int>()

    The documentation says the compiler may realise [] as Array.Empty<T>() when the target will not be modified, and the run showed it did here.

  6. Question 6 of 7 IReadOnlyList<int> fixedHours = [6, 7, 8]; What can code that receives fixedHours do?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: Read its 3 hours, but not change them: the type the compiler chose is read-only

    The target type decides what is built: IList<T> and ICollection<T> get a List<T>, while IEnumerable<T>, IReadOnlyCollection<T> and IReadOnlyList<T> get a type that cannot be changed.

  7. Question 7 of 7 What does this print?

    Read the code, then choose one answer.

    int[] morning = [6, 7, 8];
    List<int> hours = [.. morning, 12];
    morning[0] = 5;
    Console.WriteLine(hours[0]);
    Show the answer: Question 7 of 7

    Answer: 6

    A collection expression evaluates its elements once and stores them in the new collection, so it is a copy, like the departures list that kept 6, 7 and 8.

Interview questions

Fresher · What is the difference between Count and Capacity of a List<T>?

Count is how many items the list holds; Capacity is the length of the array inside it, the number of items it can hold before it must allocate a larger array. Add writes into spare room while Count is below Capacity; when they are equal, the list allocates a new array, about twice as long, and copies every item into it. In a run of 1,000 Adds the capacity went 4, 8, 16 and on to 1,024, nine arrays in all. Passing the count you expect, new List<int>(1000), makes one array and no copies, and TrimExcess() gives unused room back when it is more than 10 % of the array.

  1. Fresher to mid

    A service keeps the latest 50,000 events newest-first in a List<T>: Insert(0, e) for each new event, then RemoveAt(list.Count - 1) to drop the oldest. Under load, most of its CPU time goes to these two calls. Why, and what would you use instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A class exposes its internal List<Order> as IReadOnlyList<Order> so that callers cannot change it. A plugin casts the property back to List<Order> and adds orders, which corrupts the class's totals. How do you stop this, and what does each option cost per call?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A report sorts bookings by coach with List<T>.Sort. After an unrelated change to how bookings are loaded, bookings inside each coach appear in a different order and a snapshot test fails. Nothing in the sort changed. Explain what happened and how you would make the order fixed.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A List<T> keeps its items in an array that doubles when full, so Add is amortised O(1) and a known count belongs in the constructor.
  • Reading by index and adding at the end are cheap, while Insert, RemoveAt, Contains and Remove move or compare items one by one.
  • Collection expressions, with spread elements, build lists, arrays, spans and sets, and the target type decides which.

Terms in this lesson

amortised cost
The average cost of an operation over a long run of them. List<T>.Add sometimes copies every item into a larger array, but because the array doubles, the copies average out to about one per Add, so Add is amortised O(1).
spread element
An element written .. collection inside a collection expression, such as [.. morning, 12]. It puts every element of the collection in its place, in order; the collection can be anything a foreach loop can read.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.