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);}Bread
CoffeeButternull: 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);Bread, Coffee, ButterItems: 3List<T>is in theSystem.Collections.Genericnamespace. You do not write ausingfor it: new projects add it by themselves, likeusing System;.Addadds at the end, andRemoveremoves and closes the hole: "Coffee" moves into the place of "Milk".Countis how many elements there are right now. It has no parentheses, because it is a property. It is notLength, 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);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
- cart points to a List<string> object. Inside there is an _items array with 3 places and a _size counter.
- Add puts each element in the first free place and increases _size.
- The array is full. List<T> makes a new one with 6 places, twice as many, and copies the three elements into it.
- _items now points to the new array, and nobody needs the old one. "Butter" goes into the free place.
- Remove removes "Milk", and everything after it moves one place to the left. No hole is left.
- 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
Capacityproperty 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);045 8The 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);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]);BreadIn 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]);BreadThis 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"));March invoiceCoffee with the team, March invoice, Meeting at the bank, Warehouse report3Falsetasks[i]reads, andtasks[i] = xreplaces.Insert(i, x)inserts at indexiand moves the rest to the right.IndexOf(x)searches and returns the index or-1, andContains(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, notIndexOutOfRangeException.
Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')^1counts from the end:tasks[^1]is the last task, "Warehouse report".ContainsandIndexOfcompare withEquals. 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);RemoveAt(0) removes Ivan, and Maria becomes first, at index 0. Insert(1, "Ana") inserts Ana at index 1 and pushes Peter to the right. Three are left.
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]);}- Bread- Coffee- Butter1. Bread2. Coffee3. ButterMore 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); }}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));MilkRemoveAll.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);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 twoobjectvalues 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);FalseTrueTrueThe two 127s are boxed separately, so there are two objects. Equals compares the values.
- The old
ArrayListclass (with no angle brackets, fromSystem.Collections) keeps everything asobject. 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);2That is why today you use List<int>.
- An
intcannot benull. A number that may be missing is writtenint?:
int? discount = null;Console.WriteLine(discount.HasValue);discount = 10;Console.WriteLine(discount.HasValue + " " + discount);FalseTrue 10Try 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.
RemoveAt(i) removes by index: RemoveAt(2) removes the element at index 2, which is seat 7. Remove removes by value: seats.Remove(2). In C# the two methods have different names, so Remove(2) always means the value 2. But with numbers it is easy to mix up which of the two you need, because both the index and the value are numbers.More precisely, if you are curious
- If the value is not there,
Removedoes not stop the program and removes nothing. It returnsfalse:
List<int> seats = new List<int> { 12, 7, 5 };Console.WriteLine(seats.Remove(2));Console.WriteLine(string.Join(", ", seats));False12, 7, 5The 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));Plovdiv, Sofia, Varna5, 12, 30new 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");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);4More 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 passStringComparer.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));apple, banana, Grape, PearGrape, 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
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.
Took: Under the Yoke2 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.
2026-03-31 First quarter report2026-05-20 New coffee supplier3. 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.
Collections: List<T> grows by itselfDeleted: OOPNotes: 2There 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.
41272 + 2 = Correct!3 * 4 = Correct!15 - 9 = Wrong, the answer is 6.Score: 2 out of 3More 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,listandend, and keep the list between them. The command to remove an item that is not there says "No such item."
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Encapsulation · Polymorphism · Interfaces · Exceptions
- Continues in: Stack, queue and linked list · Dictionary and HashSet · Hash codes and hash functions · Nested classes
- Tasks on this topic: Collections