Iterating Collections with ForEach and LINQ in Modern C#

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.

Basic foreach usage
// 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.

What you can and can't do in foreach
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.

LINQ iteration methods
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.ForEach examples
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.

Parallel.ForEach for concurrent processing
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.

Filtering and transformation pipeline
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.

Performance Considerations

Different iteration approaches have performance characteristics worth understanding for high-volume scenarios.

Performance comparison
int[] numbers = Enumerable.Range(1, 1000000).ToArray();

// Fastest: for loop with array
for (int i = 0; i < numbers.Length; i++)
{
    int value = numbers[i];
    // Process value
}

// Fast: foreach with array (compiler optimizes this)
foreach (int value in numbers)
{
    // Process value
}

// Slower: LINQ with deferred execution (creates enumerator)
foreach (int value in numbers.Where(n => n > 0))
{
    // Process value
}

// Watch out for repeated enumeration
var filtered = numbers.Where(n => n % 2 == 0);

// This enumerates twice (inefficient)
int count = filtered.Count();
int sum = filtered.Sum();

// Better: Enumerate once
var list = numbers.Where(n => n % 2 == 0).ToList();
int count = list.Count;
int sum = list.Sum();

// Avoid creating collections unnecessarily
// Bad: Creates intermediate list
var result1 = numbers.Where(n => n > 0).ToList().Select(n => n * 2);

// Good: Chains without materializing
var result2 = numbers.Where(n => n > 0).Select(n => n * 2);

For most everyday scenarios, readability matters more than micro-optimizations. Use LINQ and foreach freely. Optimize specific bottlenecks only when profiling shows they need it.

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.

Frequently Asked Questions (FAQ)

Can you modify collection elements inside a foreach loop?

You can modify properties of reference type objects inside foreach, but you can't reassign the loop variable or modify value type elements directly. If you need to replace elements or modify value types, use a for loop with an index instead. Adding or removing items during foreach throws an exception.

What's the difference between foreach and List.ForEach?

Foreach is a language statement that works with any IEnumerable type, while List.ForEach is a method specific to List<T> that accepts an Action delegate. Foreach is more versatile and allows break/continue statements. ForEach is more concise for simple operations but doesn't support flow control.

Does LINQ modify the original collection when iterating?

LINQ methods don't modify the original collection. They return new sequences that represent transformed data. Methods like Where and Select create lazy enumerations that execute when you iterate them. Only when you call ToList or ToArray do you materialize results into new collections.

When should I use for instead of foreach?

Use for when you need the index position, want to iterate backwards, need to modify value type elements in place, or require precise control over iteration steps. Use foreach when you simply need to process each element sequentially and don't need index-based operations.

Back to Articles