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

  1. The compiler sees List<string> and creates type-safe calls for strings. You do not need casts when reading items.
  2. The constructor creates a list object with an internal array. The array may start empty and grow when items are added.
  3. Add checks whether Count is less than Capacity. If there is room, the item goes into the next slot.
  4. If there is no room, the list allocates a larger array and copies the old elements into it before storing the new item.
  5. Insert and RemoveAt may shift many elements because the list preserves order.
  6. 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, or tasks.
  • Use foreach when reading every item, and use a for loop when you need indexes or controlled removal.
  • Avoid removing items from a list inside a foreach loop; iterate backward with a for loop or build a filtered list instead.
  • Set an initial capacity with new List<T>(capacity) when you know a large approximate size.
  • Check Count before 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

  1. Create a List<string> of three cities. Add a fourth city, remove one city by value, and print the final list with string.Join.
  2. Create a List<int> of quiz scores. Print the lowest score, highest score, and average. Hint: Sort helps for lowest and highest, or LINQ can calculate them directly.
  3. Create a List<TaskItem> or similar class of your own. Print only the unfinished items.

Summary

  • List<T> is a strongly typed, growable collection from System.Collections.Generic.
  • Count is the number of stored items; Capacity is the size of the internal array.
  • Indexes start at 0, so the last valid index is Count - 1.
  • Add, Insert, Remove, RemoveAt, Sort, and IndexOf are 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.