Choosing the Right Iteration Approach
Iterating through collections is something you do constantly in C#. The language gives you multiple ways to loop through arrays, lists, and other sequences. Understanding which approach fits your situation helps you write clearer, more efficient code.
The foreach statement provides simple, readable iteration. LINQ methods let you transform and filter data declaratively. The ForEach method on List offers a functional style for processing elements. Each has strengths and appropriate use cases.
This guide shows you when and how to use each iteration approach, what limitations they have, and patterns that make your collection processing code both clean and performant.
Mastering the Foreach Statement
The foreach statement is your go-to tool for iterating sequences. It works with any type that implements IEnumerable, giving you consistent syntax across arrays, lists, dictionaries, and custom collections.
// Arrays
string[] names = { "Alice", "Bob", "Charlie", "Diana" };
foreach (string name in names)
{
Console.WriteLine($"Hello, {name}!");
}
// Lists
List<int> numbers = new() { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
Console.WriteLine($"Number: {number}");
}
// Dictionaries
Dictionary<string, int> ages = new()
{
["Alice"] = 25,
["Bob"] = 30,
["Charlie"] = 35
};
foreach (KeyValuePair<string, int> pair in ages)
{
Console.WriteLine($"{pair.Key} is {pair.Value} years old");
}
// Simplified dictionary iteration
foreach (var (name, age) in ages)
{
Console.WriteLine($"{name} is {age} years old");
}
Foreach handles the iteration details automatically. You don't manage indices or check bounds. The loop variable is read-only, preventing accidental modifications that would break the enumeration.
Understanding Foreach Limitations
Foreach has restrictions that prevent certain operations. Knowing these boundaries helps you choose the right loop type.
List<Person> people = GetPeople();
// CAN modify properties of reference types
foreach (Person person in people)
{
person.Age += 1; // OK: Modifying object state
person.Name = person.Name.Trim(); // OK: Modifying properties
}
// CANNOT reassign the loop variable
foreach (Person person in people)
{
// person = new Person(); // Error: Loop variable is read-only
}
// CANNOT modify the collection being iterated
foreach (Person person in people)
{
if (person.Age < 18)
{
// people.Remove(person); // Throws InvalidOperationException
}
}
// Use for loop when you need to modify value types
List<int> scores = new() { 10, 20, 30 };
for (int i = 0; i < scores.Count; i++)
{
scores[i] *= 2; // OK: Can modify through index
}
// Use ToList() to safely modify while iterating
foreach (Person person in people.ToList())
{
if (person.Age < 18)
{
people.Remove(person); // OK: Iterating over a copy
}
}
When you need to modify the collection itself during iteration, create a copy with ToList() or use a for loop. Modifying the source collection while foreach iterates over it breaks the enumerator.
Iterating with LINQ Methods
LINQ provides methods that iterate collections while transforming, filtering, or aggregating data. These methods return new sequences rather than modifying originals.
List<Product> products = GetProducts();
// Where: Filter elements
var expensive = products.Where(p => p.Price > 100);
// Select: Transform elements
var names = products.Select(p => p.Name);
// OrderBy: Sort elements
var sorted = products.OrderBy(p => p.Price);
// Chain methods together
var discounted = products
.Where(p => p.Category == "Electronics")
.Where(p => p.Price > 50)
.OrderByDescending(p => p.Price)
.Select(p => new
{
p.Name,
DiscountedPrice = p.Price * 0.9m
});
// Iterate the result
foreach (var item in discounted)
{
Console.WriteLine($"{item.Name}: ${item.DiscountedPrice:F2}");
}
// Or materialize to a collection
List<string> nameList = products
.Select(p => p.Name.ToUpper())
.ToList();
LINQ methods use deferred execution. They don't process elements until you iterate the result with foreach or materialize with ToList. This lets you build complex queries without creating intermediate collections.
Using List ForEach Method
List<T> has a ForEach method that accepts an Action delegate. It provides a functional style for processing each element.
List<string> messages = new()
{
"Hello",
"World",
"From",
"C#"
};
// Simple operation on each element
messages.ForEach(m => Console.WriteLine(m));
// Call a method on each element
messages.ForEach(Console.WriteLine);
// More complex operations
List<Order> orders = GetOrders();
orders.ForEach(order =>
{
order.ProcessingDate = DateTime.Now;
order.Status = OrderStatus.Processing;
SaveOrder(order);
});
// With index using a counter (not elegant but possible)
int index = 0;
messages.ForEach(m =>
{
Console.WriteLine($"{index++}: {m}");
});
// Compare to foreach statement (more flexible)
foreach (string message in messages)
{
Console.WriteLine(message);
if (message == "World")
{
break; // Can use break/continue
}
}
List.ForEach is concise for simple operations but doesn't support break or continue statements. For control flow within iteration, use the foreach statement instead.
Parallel Iteration for Performance
When processing large collections with independent operations, parallel iteration can improve performance on multi-core processors.
using System.Collections.Concurrent;
using System.Threading.Tasks;
List<string> urls = GetUrls();
// Process URLs in parallel
Parallel.ForEach(urls, url =>
{
var content = DownloadContent(url);
ProcessContent(content);
});
// With configuration options
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 4
};
Parallel.ForEach(urls, options, url =>
{
Console.WriteLine($"Processing {url} on thread {Thread.CurrentThread.ManagedThreadId}");
ProcessUrl(url);
});
// Collecting results from parallel processing
ConcurrentBag<Result> results = new();
Parallel.ForEach(urls, url =>
{
var result = ProcessUrl(url);
results.Add(result);
});
// PLINQ alternative
var parallelResults = urls
.AsParallel()
.Select(url => ProcessUrl(url))
.ToList();
Parallel iteration works best when each operation is independent and relatively expensive. For quick operations or small collections, the overhead of parallelization might outweigh benefits.
Practical Iteration Patterns
Common scenarios benefit from specific iteration approaches. Here are patterns you'll use frequently.
List<Employee> employees = GetEmployees();
// Pattern 1: Filter, transform, and process
var summary = employees
.Where(e => e.Department == "Sales")
.Where(e => e.YearsOfService > 5)
.Select(e => new
{
e.Name,
Bonus = e.Salary * 0.1m,
e.Department
})
.OrderByDescending(e => e.Bonus);
foreach (var item in summary)
{
Console.WriteLine($"{item.Name} gets ${item.Bonus:F2} bonus");
}
// Pattern 2: Group and process
var byDepartment = employees
.GroupBy(e => e.Department);
foreach (var group in byDepartment)
{
Console.WriteLine($"\n{group.Key} Department:");
foreach (var employee in group)
{
Console.WriteLine($" - {employee.Name}");
}
}
// Pattern 3: Safe removal during iteration
List<Order> orders = GetOrders();
orders.RemoveAll(o => o.Status == OrderStatus.Cancelled);
// Alternative: Explicit copying for complex conditions
foreach (var order in orders.ToList())
{
if (ShouldRemove(order))
{
orders.Remove(order);
LogRemoval(order);
}
}
Building pipelines with LINQ methods creates readable data transformations. The intent is clear, and the code flows naturally from source to result.
Iteration Best Practices
Following these guidelines helps you choose the right iteration approach and write maintainable code.
Use foreach for simple iteration: When you just need to process each element without indices or complex control flow, foreach is the clearest choice.
Choose for when you need indices: Use for loops when element position matters or you need to iterate backwards or skip elements.
Apply LINQ for transformations: When filtering, mapping, or aggregating data, LINQ methods express intent more clearly than manual loops.
Materialize once: If you'll iterate a LINQ query multiple times, call ToList() once rather than re-executing the query.
Don't modify during foreach: Adding or removing elements while foreach runs causes exceptions. Use ToList() to iterate over a copy if you need to modify the source.
Consider parallel for heavy operations: When processing takes significant time per element and operations are independent, Parallel.ForEach can speed things up.
Avoid List.ForEach for complex logic: The ForEach method works well for simple operations, but foreach statements are more readable when you need conditionals or control flow.