C# Lists
A C# list stores multiple values of the same type, like an array, but it can grow and shrink while your program runs. Lists matter because most real programs do not know the exact number of items in advance: users add tasks, carts add products, logs receive entries, and search results vary.
The most common list type is List<T>, pronounced list of T. The T is replaced by the element type, such as List<int>, List<string>, or List<Order>.
Overview: How C# Lists Work
List<T> is a generic class from System.Collections.Generic. Generic means the same class can be reused with many element types while still staying strongly typed. A List<string> accepts strings, a List<decimal> accepts decimals, and the compiler rejects the wrong type before the program runs.
Internally, a List<T> uses an array. The list object keeps track of two important numbers: Count and Capacity. Count is the number of elements currently stored. Capacity is the size of the internal array currently available for storage. When you call Add and the internal array still has room, the new item is placed into the next unused slot. When the array is full, the list allocates a larger array, copies the existing items into it, and then adds the new item.
This resizing behavior is why lists are easier than arrays for changing collections. You do not manually create a bigger array every time the collection grows. However, resizing has a cost: allocating memory and copying elements. For normal application code this is usually fine, but if you know you will store a large number of items, setting an initial capacity can reduce extra copying.
Lists use zero-based indexes just like arrays. The first item is at index 0, and the last item is at index Count - 1. Reading or writing an invalid index throws ArgumentOutOfRangeException. A list also preserves insertion order unless you deliberately sort or rearrange it.
A list is a reference type. Assigning one list variable to another copies the reference, not all items. If two variables refer to the same list, changes through either variable affect the same underlying collection.
Syntax
List<type> name = new List<type>();
List<type> name = new List<type> { value1, value2 };
name.Add(value);
name.Insert(index, value);
name.Remove(value);
name.RemoveAt(index);
type item = name[index];
| Part | Meaning |
|---|---|
List<type> |
A strongly typed growable collection whose elements all have the specified type. |
new List<type>() |
Creates an empty list. |
{ value1, value2 } |
A collection initializer that creates the list and adds the listed values. |
Add |
Appends one item to the end. |
Insert |
Places an item at a specific index and shifts later items right. |
Remove |
Removes the first matching value and returns whether anything was removed. |
RemoveAt |
Removes the item at a specific index. |
Count |
The number of items currently in the list. |
Modern C# also supports collection expressions such as List<int> numbers = [1, 2, 3];, but the constructor and initializer syntax above is still common and clear for beginners.
Examples
Creating, Adding, and Reading Items
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> names = new List<string> { "Ada", "Grace" };
names.Add("Linus");
names.Insert(1, "Maya");
Console.WriteLine($"Count: {names.Count}");
Console.WriteLine($"First: {names[0]}");
Console.WriteLine(string.Join(", ", names));
}
}
Output:
Count: 4
First: Ada
Ada, Maya, Grace, Linus
The initializer creates two items. Add appends Linus at the end, while Insert(1, "Maya") places Maya at index 1 and shifts Grace and Linus to the right. Count reports how many items are actually stored.
Removing Items and Calculating a Total
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<decimal> prices = new List<decimal> { 12.50m, 5.00m, 7.25m, 5.00m };
bool removed = prices.Remove(5.00m);
prices.RemoveAt(0);
decimal total = 0m;
foreach (decimal price in prices)
{
total += price;
}
Console.WriteLine($"Removed matching price: {removed}");
Console.WriteLine($"Items left: {prices.Count}");
Console.WriteLine("Total: " + total.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture));
}
}
Output:
Removed matching price: True
Items left: 2
Total: 12.25
Remove deletes only the first matching value, so one 5.00m remains until another removal happens. RemoveAt(0) removes by position, not by value. The foreach loop is ideal when you need each value but do not need to change indexes manually.
Searching, Sorting, and Filtering
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> scores = new List<int> { 88, 72, 95, 72, 100 };
scores.Sort();
int firstPerfect = scores.IndexOf(100);
List<int> passing = scores.Where(score => score >= 80).ToList();
Console.WriteLine(string.Join(", ", scores));
Console.WriteLine($"Index of 100: {firstPerfect}");
Console.WriteLine($"Passing: {string.Join(" | ", passing)}");
}
}
Output:
72, 72, 88, 95, 100
Index of 100: 4
Passing: 88 | 95 | 100
Sort changes the existing list in place. IndexOf returns the first matching index, or -1 if the value is absent. The LINQ Where call does not modify scores; ToList creates a new list containing only the values that pass the condition.
Lists of Objects
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<TaskItem> tasks = new List<TaskItem>
{
new TaskItem("Write outline", true),
new TaskItem("Record demo", false),
new TaskItem("Publish lesson", false)
};
foreach (TaskItem task in tasks)
{
string status = task.Done ? "done" : "open";
Console.WriteLine($"{task.Title}: {status}");
}
}
}
class TaskItem
{
public TaskItem(string title, bool done)
{
Title = title;
Done = done;
}
public string Title { get; }
public bool Done { get; }
}
Output:
Write outline: done
Record demo: open
Publish lesson: open
Lists are not limited to built-in types. This example stores TaskItem objects. The list stores references to those objects, so each element can expose properties and behavior. In larger programs, lists of objects are common for rows from a database, view models, game entities, and domain records.
How It Works Step by Step
- The compiler sees
List<string>and creates type-safe calls for strings. You do not need casts when reading items. - The constructor creates a list object with an internal array. The array may start empty and grow when items are added.
Addchecks whetherCountis less thanCapacity. If there is room, the item goes into the next slot.- If there is no room, the list allocates a larger array and copies the old elements into it before storing the new item.
InsertandRemoveAtmay shift many elements because the list preserves order.- The garbage collector eventually reclaims old internal arrays and list objects when no live references point to them.
Indexing a list is fast because it indexes the internal array. Adding at the end is usually fast, but inserting or removing near the beginning is slower for large lists because later items must move. If you often add and remove at both ends, another collection type may fit better.
Common Mistakes
Forgetting the Namespace
List<string> names = new List<string>();
names.Add("Ada");
This snippet does not compile by itself unless System.Collections.Generic is imported or the type is fully qualified. In full programs, add the proper using directive.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> names = new List<string>();
names.Add("Ada");
Console.WriteLine(names[0]);
}
}
Output:
Ada
Using Count as an Index
A list with three items has valid indexes 0, 1, and 2. Writing items[items.Count] asks for one position past the end. Use Add to append a new item, or use Count - 1 to read the last item.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 10, 20, 30 };
numbers.Add(40);
Console.WriteLine(numbers[numbers.Count - 1]);
Console.WriteLine(string.Join(", ", numbers));
}
}
Output:
40
10, 20, 30, 40
Putting the Wrong Type in a List
List<int> counts = new List<int> { 1, 2, 3 };
counts.Add("four");
This does not compile because counts is a List<int>. Type safety is a feature: it prevents a mixed collection from surprising the rest of your code.
Best Practices
- Use
List<T>when the collection size changes and order matters. - Use arrays when the size is fixed and you want the simplest indexed storage.
- Name list variables with clear plural nouns such as
scores,orders, ortasks. - Use
foreachwhen reading every item, and use aforloop when you need indexes or controlled removal. - Avoid removing items from a list inside a
foreachloop; iterate backward with aforloop or build a filtered list instead. - Set an initial capacity with
new List<T>(capacity)when you know a large approximate size. - Check
Countbefore reading the first or last item from a list that might be empty. - Remember that assignment copies the list reference, not the elements. Use
new List<T>(oldList)for a shallow copy.
Practice Exercises
- Create a
List<string>of three cities. Add a fourth city, remove one city by value, and print the final list withstring.Join. - Create a
List<int>of quiz scores. Print the lowest score, highest score, and average. Hint:Sorthelps for lowest and highest, or LINQ can calculate them directly. - Create a
List<TaskItem>or similar class of your own. Print only the unfinished items.
Summary
List<T>is a strongly typed, growable collection fromSystem.Collections.Generic.Countis the number of stored items;Capacityis the size of the internal array.- Indexes start at
0, so the last valid index isCount - 1. Add,Insert,Remove,RemoveAt,Sort, andIndexOfare core list operations.- Lists preserve order but may shift items during insertion and removal.
- Use lists for changing ordered collections, and choose more specialized collection types when your access pattern needs them.
