Stackify is now BMC. Read theBlog

C# Lists: What They Are and How to Use Them

By: Stackify Team
  |  January 6, 2025
C# Lists: What They Are and How to Use Them

The C# list offers a powerful, flexible, and intuitive way to work with collections in C#. That’s probably why it’s one of the most well-known and popular ways in C# to handle multiple values.

However, many C# developers barely scratch the surface of what there is to know about this important C# class. As it turns out, there are significant performance and thread-safety implications when using lists. Also, as a C# developer, you must be aware of scenarios in which a list is not the best solution and warrants a different collection type.

In this detailed guide, you’ll learn exactly lists are, how they work, how they differ from arrays and other collection types, and much more.

Let’s dig in. 

What Is a List in C# and How Does It Differ From an Array?

Let’s start by clearing up a potential misconception. Throughout this post, when we say “list” or “C# list,” we are referring to the List<T> class, which lives in the Systems.Collections.Generic namespace.

This clarification is necessary because, in programming, the term “list” can be used in a generic way to refer to any kind of list. For instance, a linked list is a textbook data structure that is a “list,” but that’s not what we’ll be talking about here.

With that out of the way, it’s time for our definition. A C# list is a class that offers a type-safe, dynamic way to handle a collection of values in C#.

When I say it’s type safe, I mean that, like an array, a list doesn’t allow you to mix values of different types. By dynamic I mean that, unlike an array, a list enables you to add or remove elements. Also like an array, you can access the elements of a list by their index.

In a nutshell, we can say that this is the main difference between arrays and lists: arrays are fixed in their size, while lists can grow as you need more items. As you’ll learn later, this difference has important performance implications you need to be aware of.

Key Features of C# List

You’ve just read about two key features of a C# list. The first one is that you can add and remove elements from the list as needed, making C# list a great fit for scenarios in which you don’t know in advance how many elements you’ll have to handle. 

The second one is that, like arrays, a list allows you to access their elements using their zero-based indexes.

What more does the C# list have to offer?

  • Sorting capabilities. You can sort the elements from the list
  • Checking the presence of an item. It’s possible to check whether the list contains a given item.
  • Clearing the list. You can remove all of the elements from the list at once
  • Finding elements. The list class provides several different methods for you to find elements according to various needs
  • Adding and removing elements at a specified index. You can add and remove items not only from the end of the list, but also at any specified position

The list above gives you a little taste of what the C# class has to offer but is not exhaustive. Let’s dive deeper into more C# list features.

Basic Operations with C# Lists

Now it’s time to examine code examples for the capabilities above and more.

How Do I Create a List?

There are several ways to create a list in C#. The most basic one is to use the constructor to initialize an empty list:

// empty list of strings, using explicit type declaration:
List<string> names = new List<string>();

// empty list of integers, using type inference
var numbers = new List<int>();

// empty list of DateTime, using a set assignment
List<DateTime> dates = [];

The last example above uses a feature called target-typed new expression, which was introduced in C# 9.0.

What if, instead of creating an empty list, you want to initialize one already with some elements? That is possible:

// creating a list passing an array as parameter
var integers = new List<int>(new int[] { 1, 2, 3 });

// an easier syntax for initialization
var moreIntegers = new List<int> {  1, 2, 3 };

// creating a range of decimals from one to 100
IEnumerable<decimal> decimals = Enumerable
    .Range(1, 100)
    .Select(i => (decimal)i);

// Using the range to initialize the list
var decimalsList = new List<decimal>(decimals);

You can also easily convert an array—or, more technically, any type that implements the IEnumerable<T> interface—to a list:

IEnumerable<int> intArray = new int[] { 1, 2, 3 };
var intList = intArray.ToList();

C# 12 introduced a new feature called collection expressions, which allows you to initialize lists and other types of collection in a more intuitive and cleaner way:

List<int> someMoreIntegers = [1, 2, 3, 5];

Finally, there’s another way to create an empty list that you must be aware of. If you know in advance the maximum number of items your list will reach, then pass that number as the capacity in the constructor:

var emptyListWithCapacityOf20 = new List<int>(20);

We’ll explain why that’s important in more detail later. For now, just know that informing the capacity can improve your application’s performance.

How Can I Add or Remove Elements from a List in C#?

The easiest and most common way to add elements to a C# list is to call the Add() function, which adds items to the end of the list. The elements are added to the list in the order you provide them.

var numbers = new List<int>();
numbers.Add(5);
numbers.Add(2);
numbers.Add(9);
numbers.Add(-10);

Console.WriteLine(string.Join(", ", numbers)); // prints '5, 2, 9, -10'

You can also insert an element at a specific position, specified by a zero-based index. For instance, continuing the example above, let’s insert an element at the second position:

var numbers = new List<int>();
numbers.Add(5);
numbers.Add(2);
numbers.Add(9);
numbers.Add(-10);

Console.WriteLine(string.Join(", ", numbers)); // prints '5, 2, 9, -10'

numbers.Insert(1, 42);
Console.WriteLine(string.Join(", ", numbers)); // prints '5, 42, 2, 9, -10'

You can also add several elements at once by using the AddRange() method and supplying an IEnumerable<T> value:

var list1 = new List<string>() { "Monday", "Tuesday", "Wednesday" };
var list2 = new List<string>() { "Thursday", "Friday" };
list1.AddRange(list2);
Console.WriteLine(string.Join(", ", list1)); // prints 'Monday, Tuesday, Wednesday, Thursday, Friday'

How about removing an element? There are several options at your disposal:

  • Remove() – allows you to specify an item and the list will remove the first occurrence of that item, if it exists
  • RemoveAt() – removes the item at the specified index
  • RemoveAll() – removes all of the items that match the specified condition
  • RemoveRange() – removes a specified number of elements starting at a given index position
list1.Remove("Saturday"); // does nothing, since 'Saturday' isn't an element in the list
list1.Remove("Monday"); // removes 'Monday' from the list
list1.RemoveAt(0); // removes the first item on the list, which is now 'Tuesday'
list1.RemoveAll(x => x.StartsWith('F')); // removes all items which match the condition, in this case, only 'Friday'
Console.WriteLine(string.Join(", ", list1)); // prints 'Wednesday, Thursday'

The methods above have different behaviors regarding what they return and how they handle the case of the item not being found. Remove() has a return type of bool, which means it returns false when the item can’t be found. RemoveAt() has a void return type; if the specified index doesn’t exist, it throws an exception—an ArgumentOutofRangeException, to be exact. RemoveAll() has an int return type and returns the number of elements that were removed.

To wrap-up, you also have the method Clear(), which removes all of the items on the list at once.

Accessing Elements on a List

You can access elements on a list in a number of ways. First, you can access any element by their index, which starts at zero. Be aware, though, that providing an invalid index results in an exception; which is why you should always check before accessing a list when you don’t know the size.

Unlike some other programming languages—such as Python—C# list does not support supplying a negative index in order to access a list in backward order (that is, -1 for the last element, -2 for the second to last, and so on.) Since C# 8, you can access the last item on a list using ^1. See the following examples.

var groceries = new List<string> { "eggs", "milk", "tomatoes", "sugar" };
Console.WriteLine($"First item on the list: {groceries[0]}"); // displays 'eggs'

// Positive indexes (0-based)
Console.WriteLine(groceries[1]); // displays 'milk'
Console.WriteLine(groceries[3]); // displays 'sugar'

// Using ^ operator (from the end)
Console.WriteLine(groceries[^1]); // displays 'sugar' (last item)
Console.WriteLine(groceries[^2]); // displays 'tomatoes' (second-to-last)
Console.WriteLine(groceries[^4]); // displays 'eggs' (fourth-from-last, which is first)

// These will throw ArgumentOutOfRangeException:
Console.WriteLine(groceries[4]);  // Error: index 4 is beyond the end (size is 4)
Console.WriteLine(groceries[-1]); // Error: negative indexes not allowed
Console.WriteLine(groceries[^5]); // Error: ^5 is beyond the start (size is 4)
Console.WriteLine(groceries[^0]); // Error: ^0 is not valid

Knowing the length of the list—which you can obtain using the Count property—you can easily iterate through the list:

var groceries = new List<string> { "eggs", "milk", "tomatoes", "sugar" };
var size = groceries.Count;

for (int i = 0; i < size; i++)
{
    var currentItem = groceries[i];
    // does something with the item
}

You can also iterate through the list in a backward way:
var groceries = new List<string> { "eggs", "milk", "tomatoes", "sugar" };
var size = groceries.Count;

for (int i = 1; i <= size; i++)
{
    var currentItem = groceries[^i]; // iterating the list backwards
    // does something with the item
}

Most of the time, though, you don’t need to use a for loop to iterate through a list. Use the foreach loop, since it’s easier to use and results in more concise code:

var groceries = new List<string> { "eggs", "milk", "tomatoes", "sugar" };
var size = groceries.Count;

foreach (string grocerie in groceries)
{
    Console.WriteLine(grocerie);
}

Advanced Operations with C# Lists

Having worked through the most fundamental C# list features, let’s now cover operations that are a bit more advanced.

How Do You Sort a List in C#?

The List<T> class offers the Sort() method, which allows you to sort the elements of a list in place. Let’s see how it works:

var numbers = new List<int> { 8, 7, 1, 0, 9, 20, -5 };
var words = new List<string> { "wings", "apple", "books", "shield" };
Console.WriteLine(string.Join(", ", numbers)); // displays '8, 7, 1, 0, 9, 20, -5'
Console.WriteLine(string.Join(", ", words)); // displays 'wings, apple, books, shield'

numbers.Sort();
words.Sort();
Console.WriteLine(string.Join(", ", numbers)); // displays '-5, 0, 1, 7, 8, 9, 20'
Console.WriteLine(string.Join(", ", words)); // displays 'apple, books, shield, wings'

So, from the examples above, it looks like Sort() works in an intuitive way, sorting the strings alphabetically and the numbers in ascending order. But what about a custom type? For instance, suppose we have a class like the following:

public class Order
{
    public int Id { get; set; }
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public DateOnly Date { get; set; }
}

Now, let’s create a list and try to sort it:

var orders = new List<Order>
{
   new Order
   {
       Id = 1,
       Description = "Some description",
       Price = 149.99m,
       Date = new DateOnly(2024, 3, 15)
   },
   new Order
   {
       Id = 2,
       Description = "Some description",
       Price = 299.99m,
       Date = new DateOnly(2024, 1, 30)
   },
   new Order
   {
       Id = 3,
       Description = "Yet another description",
       Price = 99.99m,
       Date = new DateOnly(2024, 2, 10)
   }
};

orders.Sort();

If you try to run this, you’ll get a lovely exception:

As you can see, we’ve got a System.InvalidOperationException with the error “Failed to compare two elements in the array.” The exception has an inner exception with the message “At least one object must implement IComparable.”

What does it all mean? To make a long story short, we need to “teach” C# how to compare our objects; otherwise, how is it going to know? 

In order to teach C# how to compare objects, let’s change our class so it implements the IComparable<T> interface:

public class Order : IComparable<Order>
{
    public int Id { get; set; }
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public DateOnly Date { get; set; }

    public int CompareTo(Order? other}
    {
        if (other is null) return 1;
        if (ReferenceEquals(this, other)) return 0;
        return this.Date.CompareTo(other.Date);
    }
}

The IComparable<T> interface defines a single method, CompareTo(), which gets an instance of the same type. It must then return:

  • 0, if the instances are equal
  • 1, if the current instance comes before the other in a comparison
  • -1, if the other instance comes before the current in a comparison

In our case, we are comparing using the Date property. So, let’s test this. First, I’ll add a ToString() override to the order class:

public override string ToString()
{
    return $"Order {Id}: {Description} (${Price:F2}) on {Date:MM/dd/yyyy}";
}

Now, let’s sort and print:

orders.Sort();
foreach (var order in orders)
{
    Console.WriteLine(order.ToString());
}

(The ToString() in the example above isn’t strictly necessary, since Console.Writeline already calls ToString on any parameter it gets, but I’ve added it for easier understanding.)

And now we get a nice result:

Order 2: Some description ($299,99) on 01/30/2024
Order 3: Yet another description ($99,99) on 02/10/2024
Order 1: Some description ($149,99) on 03/15/2024

That works, but what if we wanted to sort by a different property?

As it turns out, the Sort() method has a few different overloads. One of them allows you to provide a custom comparison using a delegate. Let’s use a lambda expression to be more concise:

orders.Sort((a, b) => a.Price.CompareTo(b.Price));
foreach (var order in orders)
{
    Console.WriteLine(order);
}

And here’s what the result looks like:

Order 3: Yet another description ($99,99) on 02/10/2024
Order 1: Some description ($149,99) on 03/15/2024
Order 2: Some description ($299,99) on 01/30/2024

Finally, there’s yet another way to sort a list, obtaining a new list in the process, instead of modifying the original one in place. This approach relies on LINQ:

var ordersSortedByPrice = orders.OrderBy(x => x.Price).ToList();

This approach is very flexible, allowing you to chain several ordering clauses, including ones that order in descending order:

orders = orders
    .OrderBy(o => o.Id)
    .ThenBy(o => o.Description)
    .ThenByDescending(o => o.Price)
    .ToList();

How Do You Reverse a List?

Reversing a list in C# is quite easy. The first way is simply calling the Reverse() method, which reverses the list in place:

List<int> someNumbers = [1, 2, 3, 4];

someNumbers.Reverse();
foreach (var number in someNumbers)
{
    Console.WriteLine(number);
}

The code above prints:

4
3
2
1

You can also use an overload to reverse the items belonging to a given range. For instance, let’s now reverse only the two latest items:

someNumbers.Reverse(2, 2);

The code above means that, starting from the index 2 (the third item) we want to reverse 2 items. So, we’re going to swap 2 and 1, and this is the resulting sequence:

4
3
1
2

Finally, there’s also a LINQ Reverse() method, that returns a new IEnumerable<T> result, which can then be converted to a new list:

IEnumerable<int> someNumbers = new List<int> { 1, 2, 3, 4 };

someNumbers = someNumbers.Reverse().ToList();

How Can You Check If a List Contains a Specific Element in C#?

The easiest way to test for the presence of an item on a list is to use the Contains() method, which returns true if the element is found, and false otherwise.

Another possibility is to use the method IndexOf(). It searches for the specified element and returns the index of the first occurrence, if found. If the element isn’t found, the method returns -1.

Best Practices for Working with C# Lists

To understand best practices when working with lists, let’s recall the main properties of the C# list.

  • It is mutable, and you can add and remove items
  • It allows multiple occurrences of the same value
  • It has several methods that change the list in place

With all that in mind, here are some best practices you should be aware of:

  • Don’t pick a list when there’s a better collection available. For instance, use a HashSet<T> if you need to prevent duplicates. Use a Stack<T> in case you need LIFO capabilities
  • When iterating through a list, prefer a foreach loop in most situations, since doing so is more readable and concise
  • If you need to change the list (for instance, removing elements) prefer a reverse for loop
  • Modifications aren’t allowed in a foreach loop, and if you remove items from the list in a regular for loop, you’ll cause problems, since the indexes would change in the process
  • When passing a list to a method that’s not supposed to modify them, convert them to an immutable collection type

What Are Performance Implications of Using a List in C#?

Before wrapping up, let’s cover some of the performance implications of using lists in C#.

For starters, it’s important to understand how the list works and how it’s possible to add more items to it. Interestingly enough, the List<T> class works by having an internal array. When you create an empty list, the array has an initial capacity (which is not officially documented anywhere, as far as I know, but many sources seem to agree it’s 4.)

When adding items to the list and the array becomes full, the list object creates a new one with double the capacity and copies over the items to the new array. Can you see how this is bad? The copying process itself is already somewhat taxing, but the main problem is that the original array continues lingering there, in memory, adding pressure to the garbage collector.

The solution here is simple. If you know in advance or at least have an educated guess of how big your list will be, provide that number as an argument when you create the list:

var myList = new List<int>(50);

That way, the list will be initialized already with that capacity, and the copying process won’t be necessary as the list grows.

Here are a few more performance considerations:

  • Adding elements to the end of the list is usually efficient, but methods like RemoveAt(), Remove(), and Insert() are expensive, since they require changing the positions of elements in the internal array. Use them with caution
  • Use AddRange rather than multiple Add calls when you need to add a lot of items at once, since AddRange is optimized for such a scenario
  • Use Clear() instead of creating a new list if you’ll need the current capacity of the list again, since Clear() doesn’t reset the capacity to the original value. Also, creating a new list creates another object in memory, which can add more pressure to the garbage collector

Finally, beware of scenarios in which people use many ToList() calls unnecessarily. For instance, in a method like the following:

public IEnumerable<Order> GetActiveOrdersByCustomer(int  customerId)
{
    return _context.Orders
      .Where(x => x.Active)
      .ToList()
      .Where(x => x.CustomerId == customerId)
      .ToList();
}

The first ToList() is completely useless, because it materializes the query before it’s necessary, fetching potentially a large number of rows for further filtering in memory. Besides that, the ToList() call creates an intermediate list object, which isn’t necessary and puts more pressure into the garbage collector.

Another great practice you can adopt when it comes to optimizing performance is to use an application monitoring tool. Stackify Retrace is a solution that helps you monitor the performance of your C# applications, including but not limited to tracking slow processing times, inefficient memory use, and problems with SQL queries—such as the one generated in the above method. To see how Stackify can improve the performance of your C# applications, start your free trial today.

Conclusion

The C# list class is a powerful and flexible way of working with collections in C#. It makes use of generics to create a type-safe environment for you to handle items. You can easily add, insert, and remove items, as well as sort and manipulate the collection in interesting and useful ways.

The C# list class isn’t the perfect solution for everything, though. There are scenarios in which you’re better off with a different type altogether. As you’ve learned in the post, there are performance issues you might run into if you’re not careful with certain operations.

I hope the article was helpful. Happy coding and thanks for reading.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]