Skip to content
wannacode

Lists

In short: A list is a collection that grows and shrinks by itself; List<T> keeps the elements in order like an array, and List<string> says what type they are.

Problem

The cart is full

You are writing the shopping cart of an online store, and you keep the items in an array. But an array has a size that does not change. When the customer adds a fourth item, you make a bigger array and copy everything. When they remove an item, a hole is left:

string[] cart = { "Bread", "Milk", "Coffee" };
// A fourth item: the array is full, so we make a bigger one and copy.
string[] bigger = new string[cart.Length + 1];
for (int i = 0; i < cart.Length; i++)
{
bigger[i] = cart[i];
}
bigger[cart.Length] = "Butter";
cart = bigger;
// The customer removes the milk.
cart[1] = null;
foreach (string item in cart)
{
Console.WriteLine(item);
}
Output
Bread
Coffee
Butter
Now every loop over the cart has to skip null: Console.WriteLine prints it as an empty line. And the number of items is no longer cart.Length.

That is a lot of code for something every store does all the time.

The idea

List<T>: an array that looks after itself

C# has a ready-made class for this: List<T>. It keeps the elements in an array, but it does the copying and shifting by itself. A collection like this is called a list:

List<string> cart = new List<string>();
cart.Add("Bread");
cart.Add("Milk");
cart.Add("Coffee");
cart.Add("Butter");
cart.Remove("Milk");
Console.WriteLine(string.Join(", ", cart));
Console.WriteLine("Items: " + cart.Count);
Output
Bread, Coffee, Butter
Items: 3
  • List<T> is in the System.Collections.Generic namespace. You do not write a using for it: new projects add it by themselves, like using System;.
  • Add adds at the end, and Remove removes and closes the hole: "Coffee" moves into the place of "Milk".
  • Count is how many elements there are right now. It has no parentheses, because it is a property. It is not Length, because a list is not an array.
  • Console.WriteLine(cart) does not print the elements, only the name of the type:
List<string> cart = new List<string>();
cart.Add("Bread");
Console.WriteLine(cart);
Output
System.Collections.Generic.List`1[System.String]

That is why string.Join(", ", cart) joins the elements into one text, separated by commas.

Here is what List<T> does inside. The number in the parentheses is the starting size of the array. Without it, the array is empty, and on the first Add it gets 4 places. Here it is 3, so you can see what happens when the array fills up:

List<string> cart = new List<string>(3);
cart.Add("Bread");
cart.Add("Milk");
cart.Add("Coffee");
cart.Add("Butter");
cart.Remove("Milk");
Console.WriteLine(string.Join(", ", cart));
Console.WriteLine("Items: " + cart.Count);
Step by step, in words
  1. cart points to a List<string> object. Inside there is an _items array with 3 places and a _size counter.
  2. Add puts each element in the first free place and increases _size.
  3. The array is full. List<T> makes a new one with 6 places, twice as many, and copies the three elements into it.
  4. _items now points to the new array, and nobody needs the old one. "Butter" goes into the free place.
  5. Remove removes "Milk", and everything after it moves one place to the left. No hole is left.
  6. From the outside you see only a list of three items: List<T> does the copying and the shifting. Capacity tells you how many places it has.
More precisely, if you are curious
  • The Capacity property tells you how many places the array inside has. When it fills up, List<T> makes a new array with twice as many places:
List<string> cart = new List<string>();
Console.WriteLine(cart.Capacity);
cart.Add("Bread");
Console.WriteLine(cart.Capacity);
for (int i = 0; i < 4; i++)
{
cart.Add("Milk");
}
Console.WriteLine(cart.Count + " " + cart.Capacity);
Output
0
4
5 8

The idea

What goes in the angle brackets

List<string> means "a list of text". The type in the angle brackets is called a type parameter (generics). It tells the compiler what is in the list, and the compiler enforces it:

List<string> cart = new List<string>();
cart.Add("Bread");
cart.Add(5);
Compile error
error CS1503: Argument 1: cannot convert from 'int' to 'string'

That is why cart[0] returns a string directly, with no conversion.

On the right, the type is written once more: new List<string>(). There is also a short form, new(). Then the compiler takes the type from the left side:

List<string> cart = new();
cart.Add("Bread");
Console.WriteLine(cart[0]);
Output
Bread

In C#, the variable usually has the class itself as its type, List<string>. The list also has an interface, IList<string>, with an I in front like every interface in C#. It says what a list can do, and List<string> is one way of doing it:

IList<string> cart = new List<string>();
cart.Add("Bread");
Console.WriteLine(cart[0]);
Output
Bread

This is the polymorphism from the "Inheritance and polymorphism" module. You will most often see the interface as the type of a parameter.

The idea

Indexes: [i], Insert, IndexOf

A list has indexes like an array, from 0 to Count - 1, and you write them with the same [ ]:

List<string> tasks = new List<string>();
tasks.Add("March invoice");
tasks.Add("Call the bank");
tasks.Add("Warehouse report");
Console.WriteLine(tasks[0]);
tasks[1] = "Meeting at the bank";
tasks.Insert(0, "Coffee with the team");
Console.WriteLine(string.Join(", ", tasks));
Console.WriteLine(tasks.IndexOf("Warehouse report"));
Console.WriteLine(tasks.Contains("Call the bank"));
Output
March invoice
Coffee with the team, March invoice, Meeting at the bank, Warehouse report
3
False
  • tasks[i] reads, and tasks[i] = x replaces.
  • Insert(i, x) inserts at index i and moves the rest to the right.
  • IndexOf(x) searches and returns the index or -1, and Contains(x) only says whether it is there.
More precisely, if you are curious
  • An index outside the list stops the program. The exception is different from the one for an array: ArgumentOutOfRangeException, not IndexOutOfRangeException.
tasks[3] with 3 tasks
Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
  • ^1 counts from the end: tasks[^1] is the last task, "Warehouse report".
  • Contains and IndexOf compare with Equals. For text that means "the same characters", so they find text that is equal, not only the same object.
  • Inserting and removing at the front are slow in a big list: every element after the index moves by one place. For a million elements, that is a million moves.

Try it

Who is next?

The list of patients waiting at the doctor's. Ivan goes in, and Ana has an appointment, so she takes second place.

What will the program print?

List<string> waiting = new List<string>();
waiting.Add("Ivan");
waiting.Add("Maria");
waiting.Add("Peter");
waiting.RemoveAt(0);
waiting.Insert(1, "Ana");
Console.WriteLine(string.Join(", ", waiting) + " " + waiting.Count);
Answer

The idea

Looping over the elements

You loop over a list like an array: with foreach when you need only the elements, or with for and an index when you also need the number:

List<string> cart = new List<string>();
cart.Add("Bread");
cart.Add("Coffee");
cart.Add("Butter");
foreach (string item in cart)
{
Console.WriteLine("- " + item);
}
for (int i = 0; i < cart.Count; i++)
{
Console.WriteLine((i + 1) + ". " + cart[i]);
}
Output
- Bread
- Coffee
- Butter
1. Bread
2. Coffee
3. Butter
More precisely, if you are curious
  • Do not remove elements from a list while you loop over it with foreach. The program stops:
foreach (string item in cart)
{
if (item == "Bread")
{
cart.Remove(item);
}
}
Error
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

The exception always comes, even when you remove the second to last element: on the next step, foreach checks whether the list has changed.

  • If you have to remove, loop with an index from the back to the front. That way, removing does not shift the elements you have not checked yet:
List<string> cart = new List<string>();
cart.Add("Bread");
cart.Add("Milk");
cart.Add("Bread");
for (int i = cart.Count - 1; i >= 0; i--)
{
if (cart[i] == "Bread")
{
cart.RemoveAt(i);
}
}
Console.WriteLine(string.Join(", ", cart));
Output
Milk
In the "Modern C#" module you will see a shorter way: RemoveAll.

The idea

Numbers in a list: List<int>

A list of scores is a List<int>. The angle brackets can also take a primitive type like int, double or bool, so the number sits in the list as it is:

List<int> scores = new List<int>();
scores.Add(85);
scores.Add(64);
scores.Add(91);
int total = 0;
foreach (int score in scores)
{
total += score;
}
Console.WriteLine("Average: " + (double) total / scores.Count);
Output
Average: 80

(double) turns the sum into a fractional number, so the division is not integer division. C# prints 80, not 80.0: a fractional number with no fractional part comes out without a point.

More precisely, if you are curious
  • C# has boxing too. When a primitive type goes into a variable of type object, C# puts it in an object. And == on two object values checks whether they point to the same object:
object a = 127;
object b = 127;
Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));
int c = 127;
int d = 127;
Console.WriteLine(c == d);
Output
False
True
True

The two 127s are boxed separately, so there are two objects. Equals compares the values.

  • The old ArrayList class (with no angle brackets, from System.Collections) keeps everything as object. It takes both a number and text, and it boxes every number:
ArrayList old = new ArrayList();
old.Add(85);
old.Add("Bread");
Console.WriteLine(old.Count);
Output
2

That is why today you use List<int>.

  • An int cannot be null. A number that may be missing is written int?:
int? discount = null;
Console.WriteLine(discount.HasValue);
discount = 10;
Console.WriteLine(discount.HasValue + " " + discount);
Output
False
True 10

Try it

Find the bug

A theater keeps the taken seats in a list. A customer gives up seat 2, and the program should print 12, 7, 5. Instead it prints 12, 2, 5. Which line has the bug?

Pick the line with the bug.

More precisely, if you are curious
  • If the value is not there, Remove does not stop the program and removes nothing. It returns false:
List<int> seats = new List<int> { 12, 7, 5 };
Console.WriteLine(seats.Remove(2));
Console.WriteLine(string.Join(", ", seats));
Output
False
12, 7, 5

The idea

Sorting and a read-only list

Sort() sorts the list in place: text in alphabetical order, numbers by size.

List<string> cities = new List<string>();
cities.Add("Sofia");
cities.Add("Plovdiv");
cities.Add("Varna");
cities.Sort();
Console.WriteLine(string.Join(", ", cities));
List<int> prices = new List<int> { 30, 5, 12 };
prices.Sort();
Console.WriteLine(string.Join(", ", prices));
Output
Plovdiv, Sofia, Varna
5, 12, 30

new List<int> { 30, 5, 12 } makes a list from the values you list. It is an ordinary list, and you can change it.

A list that does not change is declared with the IReadOnlyList<T> interface. It only has the members that read: the indexer and Count. So the compiler does not let you add, remove or sort:

IReadOnlyList<string> days = new List<string> { "Mon", "Tue", "Wed" };
days.Add("Thu");
Compile error
error CS1061: 'IReadOnlyList<string>' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'IReadOnlyList<string>' could be found (are you missing a using directive or an assembly reference?)

IReadOnlyList does not make a copy. It only hides the members that change the list. If someone has the List itself, their change shows through days too:

List<string> week = new List<string> { "Mon", "Tue", "Wed" };
IReadOnlyList<string> days = week;
week.Add("Thu");
Console.WriteLine(days.Count);
Output
4
More precisely, if you are curious
  • Sort() sorts text by the rules of the language set on the computer (the culture). Capital and lowercase letters are not kept apart, so "apple" comes before "Pear". If you pass StringComparer.Ordinal, text is sorted by the codes of its characters, and capital letters come before lowercase ones:
List<string> fruit = new List<string> { "apple", "Pear", "banana", "Grape" };
fruit.Sort();
Console.WriteLine(string.Join(", ", fruit));
fruit.Sort(StringComparer.Ordinal);
Console.WriteLine(string.Join(", ", fruit));
Output
apple, banana, Grape, Pear
Grape, Pear, apple, banana
  • How to sort your own objects, for example products by price, you will see in the lesson "Nested classes".

Exercise

Lists in classes

Solve the tasks in your editor. The list is a private field of the class, and Main only calls its methods.

1. A bookshelf. A BookShelf class with a AddBook(string title) method that puts the book at the end, and TakeBook(), which returns and removes the first book (or null if the shelf is empty). Add a property Count and ToString(). In Main, add "Under the Yoke", "Tobacco" and "The Iron Candlestick", take one book and print the rest.

Output
Took: Under the Yoke
2 left: [Tobacco, The Iron Candlestick]

2. An event log. An Event class with a date and a description, and an EventLog class with the methods AddEvent(string date, string description) and GetEventsBetween(string from, string to), which returns a list of the events between the two dates, inclusive. The dates are text in the form 2026-03-31. In this form, the text sorts the same way as the dates, so string.CompareOrdinal(a, b) compares them correctly. It returns a negative number if a comes before b, 0 if they are equal and a positive number if a comes after b. Add four events: 2026-01-15 "Annual stocktake", 2026-03-31 "First quarter report", 2026-05-20 "New coffee supplier", 2026-09-01 "New school year". Print the ones from 2026-03-01 to 2026-06-30.

Output
2026-03-31 First quarter report
2026-05-20 New coffee supplier

3. A notebook. A Note class with a title and content, and a Notebook class with the methods AddNote, FindNote(string title) (returns the note or null), DeleteNote(string title) (returns true if it deleted something) and a property Count. Add the notes "OOP", "Collections" and "SQL", find "Collections", delete "OOP" and check that it is gone.

Output
Collections: List<T> grows by itself
Deleted: OOP
Notes: 2
There is no OOP note.

4. Mental math. A Question class with a text and the right answer, and a MathGame class with a list of questions, an AddQuestion method and a Start() method. Start asks the questions one by one, reads the answer and at the end says how many were right. The questions are 2 + 2, 3 * 4 and 15 - 9.

Input
4
12
7
Output
2 + 2 = Correct!
3 * 4 = Correct!
15 - 9 = Wrong, the answer is 6.
Score: 2 out of 3

More tasks

  • No repeats. Read 6 names and collect them in a list, skipping the ones that are already in it. For the input Ivan, Maria, Ivan, Ana, Maria, Peter the output is Ivan, Maria, Ana, Peter.
  • The most expensive item. A List<double> holds prices. Find the highest one and its index, without Max.
  • Reverse the list. Write a method that returns a new list with the same elements in reverse order. Do not change the original.
  • A shopping list. Read the commands add milk, remove milk, list and end, and keep the list between them. The command to remove an item that is not there says "No such item."

Test

Check yourself

1. Why is a List<T> handier than an array for a shopping cart?
2. What does <string> say in List<string> cart?
3. List<int> seats holds 12, 2, 7, 5. What is left after seats.Remove(2)?
4. Can you write List<int>?
5. IReadOnlyList<string> days = new List<string> { "Mon", "Tue" }; What happens at days.Add("Wed")?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·