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.
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:
| Aspect | foreach | for loop |
|---|---|---|
| Syntax | Clean, no index | Requires index variable |
| Modification | Cannot modify list | Can modify list safely |
| Performance | Slightly slower due to enumerator | Faster for simple index access |
| Readability | Better for simple iteration | Better 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,
foreachthrows aNullReferenceException. Always check for null if the list comes from external input. - Empty list:
foreachsimply does nothing; no exception is thrown. - Modifying elements: You cannot assign to the loop variable. To update an element, use a
forloop. - 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.AsSpanto get a span from aList<T>and iterate with aforloop.
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#.