Back to Blog
C#

C# List foreach: Syntax, Pitfalls, and Performance

c# list foreach: Learn how to use foreach with List<T> in C#, including syntax, enumerator behavior, common pitfalls, and performance tradeoffs.

C#ListforeachIEnumerableLINQPerformance
Illustration of a C# foreach loop iterating over a list with an enumerator pointer.

The C# list foreach pattern is the most common way to iterate over a List<T>. It provides a clean, readable syntax that hides the complexity of the enumerator. However, using foreach with a List<T> has specific behaviors and limitations that can affect correctness and performance. This article covers the syntax, how it works under the hood, common mistakes, and when to choose alternative iteration methods.

Basic foreach Syntax on List<T>

Iterating over a List<T> with foreach is straightforward:

List<string> names = new List<string> { "Alice", "Bob", "Charlie" }; foreach (string name in names) { Console.WriteLine(name); }

The loop variable name is read-only. You cannot assign to it to change the corresponding element in the list. If you need to modify elements, you must use a for loop or access the index directly.

How foreach Works with List<T>

The foreach statement is syntactic sugar for accessing the enumerator. For a List<T>, the compiler calls GetEnumerator(), which returns a List<T>.Enumerator struct. This struct implements IEnumerator<T> and is optimized to avoid heap allocation when used directly.

The loop expands to something like this:

using (var enumerator = names.GetEnumerator()) { while (enumerator.MoveNext()) { string name = enumerator.Current; Console.WriteLine(name); } }

The using statement ensures Dispose() is called, which is important for enumerators that hold resources. For List<T>, disposal is trivial, but the pattern is consistent across all collection types.

Modifying a List During foreach Iteration

If you attempt to add, remove, or replace elements in the list inside a foreach loop, you'll get an InvalidOperationException at the next MoveNext() call. The list tracks a version number that changes on modification, and the enumerator checks it.

List<int> numbers = new List<int> { 1, 2, 3 }; foreach (int number in numbers) { if (number == 2) { numbers.Add(4); // Throws InvalidOperationException } }

This is a common mistake. To modify a list while iterating, you can iterate backward with a for loop, or collect changes and apply them after the loop.

Performance: foreach vs for Loop

For a List<T>, a for loop with an index is often slightly faster than foreach because it avoids the enumerator's MoveNext() and Current calls and the associated version check. However, the difference is usually negligible for most applications. The foreach loop is more readable and less error-prone.

The enumerator for List<T> is a struct, so it does not allocate on the heap when used directly. But if you treat it as IEnumerator<T> (for example, in a generic method), boxing occurs, adding overhead. In hot paths, consider using a for loop or a span-based iteration.

Here is a comparison:

Aspectforeachfor loop
SyntaxClean, no indexRequires index variable
ModificationCannot modify listCan modify list safely
PerformanceSlightly slower due to enumeratorFaster for simple index access
ReadabilityBetter for simple iterationBetter when index is needed

Use foreach when you only need to read each element. Use for when you need to modify elements, access the index, or iterate in reverse.

Using LINQ with List<T>

LINQ provides the ForEach method on List<T>, but it is not the same as the foreach statement. List<T>.ForEach takes an Action<T> and executes it for each element. However, it is rarely the best choice because it is less readable and cannot break early.

names.ForEach(name => Console.WriteLine(name));

This is equivalent to a foreach loop but offers no advantage. The foreach statement is more idiomatic and supports break and continue. LINQ's Select and Where are better for transforming and filtering, but they return deferred queries, not immediate execution.

Common Mistakes and Edge Cases

  • Null list: If the list is null, foreach throws a NullReferenceException. Always check for null if the list comes from external input.
  • Empty list: foreach simply does nothing; no exception is thrown.
  • Modifying elements: You cannot assign to the loop variable. To update an element, use a for loop.
  • Capturing the loop variable in a lambda: In C# 5 and later, the loop variable is captured per iteration, so it is safe to use in lambdas. Earlier versions had a known issue where the variable was captured by reference.

When to Use Alternative Iteration Approaches

  • for loop: Use when you need the index, modify the list, or iterate in reverse.
  • while loop: Use when the iteration condition is not based on the collection size.
  • List<T>.ForEach: Rarely useful; prefer foreach.
  • Span<T> and Memory<T>: For high-performance scenarios where you need to avoid allocations and work with contiguous memory. You can use CollectionsMarshal.AsSpan to get a span from a List<T> and iterate with a for loop.
Span<int> span = CollectionsMarshal.AsSpan(numbers); for (int i = 0; i < span.Length; i++) { // Direct access without enumerator overhead }

This is an advanced optimization and should be used only when profiling shows a bottleneck. The foreach statement remains the clearest and safest default for most list iteration in C#.

c# list foreach: Practical Usage and Code Examples | RYUSLOG DEV