C# Arrays Vs Lists

Arrays and lists both store ordered collections of values in C#, but they are built for different jobs. An array has a fixed length once created, while List<T> can grow and shrink as your program runs. Understanding the difference matters because it affects readability, performance, memory use, and whether your code is easy to change later.

Overview: How Arrays And Lists Work

An array is a built-in CLR type that stores a fixed number of elements of one type. When you create new int[5], the runtime allocates one object large enough to hold five int values plus object metadata. The length is part of the array object and cannot be changed. You can replace values at existing indexes, but you cannot add a sixth element to that same array.

A List<T> is a generic collection class from System.Collections.Generic. It gives you a resizable sequence with methods such as Add, Remove, Insert, and Contains. Internally, a list uses an array to store its elements. The important difference is that the list manages that internal array for you. When the current internal array is full and you call Add, the list allocates a larger array, copies the existing elements, and then stores the new item.

Both arrays and lists are zero-indexed, so the first element is at index 0. Both preserve insertion order. Both can be used with for and foreach. Both are reference types when stored in variables, even if their elements are value types like int. That means assigning an array or list variable to another variable copies the reference, not all the elements.

The biggest everyday rule is simple: use an array when the size is naturally fixed or when an API requires an array; use List<T> when the number of items changes. For example, the seven days of a week fit an array well. A shopping cart, search results, or user-selected tags fit a list better.

Feature Array List<T>
Size Fixed after creation Can grow and shrink
Length property Length Count
Add/remove methods No built-in resizing methods Add, Remove, Insert, Clear
Index access Fast: items[i] Fast: items[i]
Internal storage The storage itself A wrapper around an internal array

Syntax

int[] scores = new int[3];
scores[0] = 90;

List<int> numbers = new List<int>();
numbers.Add(10);
  • int[] means an array whose elements are int values.
  • new int[3] creates exactly three slots, initialized to 0 because int is a value type.
  • List<int> means a generic list whose elements are int values.
  • Add appends a value to the end of a list and increases its Count.
  • Use Length for arrays and Count for lists. Mixing them up is a common beginner error.

Examples

Example 1: A Fixed Set Of Scores

using System;

class Program
{
    static void Main()
    {
        int[] scores = { 92, 81, 100 };

        Console.WriteLine($"Number of scores: {scores.Length}");
        Console.WriteLine($"First score: {scores[0]}");

        scores[1] = 85;
        Console.WriteLine($"Updated second score: {scores[1]}");
    }
}

Output:

Number of scores: 3
First score: 92
Updated second score: 85

This program uses an array because the number of scores is known at creation time. The array has three elements forever, but each element can be changed. The statement scores[1] = 85 replaces the second value because index 1 is the second position.

Example 2: A List That Grows

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> tasks = new List<string>();

        tasks.Add("Write outline");
        tasks.Add("Review examples");
        tasks.Add("Publish lesson");
        tasks.Remove("Review examples");

        Console.WriteLine($"Task count: {tasks.Count}");
        foreach (string task in tasks)
        {
            Console.WriteLine(task);
        }
    }
}

Output:

Task count: 2
Write outline
Publish lesson

A list is the better choice here because tasks are added and removed over time. The list starts empty, grows to three items, then shrinks to two. The code does not need to create a replacement array manually; List<string> handles storage changes internally.

Example 3: Converting Between Arrays And Lists

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        string[] original = { "csharp", "dotnet", "arrays" };
        List<string> editable = original.ToList();

        editable.Add("lists");
        string[] finalTags = editable.ToArray();

        Console.WriteLine(string.Join(", ", finalTags));
        Console.WriteLine($"Original length: {original.Length}");
        Console.WriteLine($"Final length: {finalTags.Length}");
    }
}

Output:

csharp, dotnet, arrays, lists
Original length: 3
Final length: 4

Converting is useful when one part of an API wants an array but your code is easier to write with a list. ToList creates a new list containing copies of the references or values from the array. ToArray creates a new array. The original array is not resized or modified by adding to the list.

Example 4: Capacity Is Not Count

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int>(capacity: 5);

        Console.WriteLine($"Count before add: {numbers.Count}");
        Console.WriteLine($"Capacity before add: {numbers.Capacity}");

        numbers.Add(10);
        numbers.Add(20);

        Console.WriteLine($"Count after add: {numbers.Count}");
        Console.WriteLine($"Capacity after add: {numbers.Capacity}");
    }
}

Output:

Count before add: 0
Capacity before add: 5
Count after add: 2
Capacity after add: 5

Count is how many real elements the list currently contains. Capacity is the size of the internal array the list has reserved. Creating a list with capacity 5 does not create five usable elements; it only avoids some future reallocations if you expect to add about five items.

How It Works Step By Step

When you create an array, the CLR allocates a single array object. The runtime knows the element type and the length, so index access can be performed by calculating where the requested element lives inside that contiguous block. C# also performs bounds checking. If you try to read scores[3] from a three-element array, the valid indexes are only 0, 1, and 2, so the runtime throws IndexOutOfRangeException.

When you create a list, the list object has fields such as an internal array and a count. Calling Add checks whether the internal array has enough capacity. If there is room, the item is stored at the next index and Count increases. If there is no room, the list allocates a bigger internal array, copies the old elements, then adds the new item. This makes most appends fast, but an occasional append costs more because copying is required.

Removing from the end of a list is cheap. Removing from the middle is more expensive because elements after the removed item must shift left to keep the list contiguous. Arrays have the same shifting problem if you manually simulate removal, but arrays do not provide resizing methods because their length is fixed.

Common Mistakes

Using List Syntax On An Array

int[] values = { 1, 2, 3 };
values.Add(4);

This does not compile because arrays do not have an Add method. If the collection must grow, use a list:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> values = new List<int> { 1, 2, 3 };
        values.Add(4);
        Console.WriteLine(string.Join(", ", values));
    }
}

Output:

1, 2, 3, 4

Confusing Capacity With Existing Elements

List<int> values = new List<int>(3);
Console.WriteLine(values[0]);

This compiles, but it throws ArgumentOutOfRangeException at runtime because the list has capacity for three elements but Count is still zero. Add elements before reading them:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> values = new List<int>(3);
        values.Add(42);
        Console.WriteLine(values[0]);
    }
}

Output:

42

Forgetting That Assignment Shares The Same Collection

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> first = new List<string> { "red", "green" };
        List<string> second = first;

        second[0] = "blue";
        Console.WriteLine(first[0]);
    }
}

Output:

blue

Both variables refer to the same list object. To make an independent copy, use new List<string>(first) for a list or array.ToArray() for an array.

Best Practices

  • Choose an array when the length is fixed by the problem, such as months, days, coordinates, or a buffer returned by an API.
  • Choose List<T> when items are added, removed, filtered, or collected over time.
  • Use Length for arrays and Count for lists.
  • If you know approximately how many items a list will hold, pass an initial capacity to reduce resizing work.
  • Do not expose mutable arrays or lists from classes unless callers are supposed to modify them. Prefer read-only views in public APIs when appropriate.
  • Remember that both arrays and lists are reference types. Assignment does not automatically clone the collection.
  • Use foreach when you only need the values; use a for loop when you need indexes or need to update elements by position.

Practice Exercises

  1. Create an array of five temperatures, print the highest value, then update one temperature and print the array again.
  2. Create a List<string> for a playlist. Add four songs, remove one, and print the remaining songs with their position numbers.
  3. Write a program that starts with an array of product codes, converts it to a list, adds a new code, converts it back to an array, and prints the final length.

Summary

  • Arrays are fixed-size collections with fast index access and a Length property.
  • List<T> is a resizable collection built on top of an internal array and uses a Count property.
  • List capacity is reserved storage, not the number of usable elements.
  • Adding to a full list creates a larger internal array and copies existing elements.
  • Use arrays for naturally fixed data and lists for data that changes size.