In this article, we’ll delve into the world of C# iteration and explore the pros and cons of both the time-tested foreach statement and the relatively unknown ForEach() method.

To download the source code for this article, you can visit our GitHub repository.

Let’s start!

What Do the ForEach Method and Foreach Statement Do in C#?

Both the ForEach() method and foreach statement can be used to iterate over each individual item in a collection. 

The ForEach Method

We can only use the ForEach() method with a limited set of collections in C#. Those are ImmutableList<T> and List<T> where the ForEach() method takes in an Action<T> as a parameter. 

The Array class also has a similar method – ForEach<T>(T[], Action<T>).

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The ForEach() method should not be mistaken for the Parallel.ForEach() method that may run a foreach operation in parallel.

All three versions of the method behave the same way but only differ with the parameters they take in. We cannot use any complex logic inside the method such as the break and continue keywords to stop the iteration or skip an item.

Let’s see how the ForEach() method works:

public static class Iterators
{
    public static int GetTotalOfIntListWithForEachMethod(List<int> prices)
    {
        int total = 0;

        prices.ForEach(x => total += x);

        return total;
    }
}

In the Iterators class, we define the GetTotalOfIntListWithForEachMethod() method. It takes in a List<int> and is responsible for calculating and returning the total sum of the list’s elements. The Action<T> that we pass as a parameter to the ForEach() method is a lambda expression that adds the current element to the total variable.

We can check its implementation as well, which can be found on GitHub:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);
    }

    int version = _version;

    for (int i = 0; i < _size; i++)
    {
        if (version != _version)
        {
            break;
        }
        action(_items[i]);
    }

    if (version != _version)
        ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
}

At first glance, we see that the ForEach() method uses a for statement to loop through the collection. But we gain other insightful information as well. We have a local integer version variable that is initially equal to the _version field. This is used to track whether the list changes during the execution of the method and if so we break out of the loop and get an exception.

Another important thing is the fact that the Action<T> is carried on the _items field, which is a T[] holding the items of the collection. This can theoretically lead to improved performance.

The Foreach Statement

We can use the foreach statement to enumerate the elements of a collection and execute its body for each element of the collection. It can be used on any type that implements the IEnumerable or IEnumerable<T> interface. We can also use it with any custom types that have a public parameterless GetEnumerator() method, the return type of the GetEnumerator() method has the public Current property, and the public parameterless MoveNext() method whose return type is bool. The GetEnumerator() method can also be an extension method starting from C# 9.0.

The foreach statement is more versatile as we can have complex logic inside the code block and we can break out of the loop using the break keyword. We can also step to the next iteration in the loop using the continue keyword.

Let’s check how the above example can be implemented using the foreach statement:

public static int GetTotalOfIntListWithForeachStatement(List<int> prices)
{
    int total = 0;

    foreach (var price in prices)
    {
        total += price;
    }

    return total;
}

Here, we create the GetTotalOfIntListWithForeachStatement() method. Inside it, we use the foreach statement to iterate over the prices and add each individual price to the total variable.

We should be careful when using the foreach statement not to make changes to the collection itself. The GetEnumerator() method returns an IEnumerator<T> instance which doesn’t have complete access to the collection. The returned enumerator remains valid as long as the collection remains unchanged. Making changes to the collection, such as adding or deleting elements, while iterating over it invalidates the enumerator, possibly leading to unexpected results. 

How Do the ForEach Method and Foreach Statement Differ Behind the Scenes?

To get a better understanding of how each iteration method works, we will use a tool called SharpLab to look behind the scenes at the lowered C# code.

Lowered C# Code for the ForEach Method

The ForEach() method has two distinct parts in the lowered C# version of the code:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int total;

    internal void <GetTotalOfIntListWithForEachMethod>b__0(int x)
    {
        total += x;
    }
}

The compiler generates the <>c__DisplayClass0_0 class and decorates it with the [CompilerGenerated] attribute. Then, the class itself has one public property of int type called total. It also generates the <GetTotalOfIntListWithForEachMethod>b__0() method, which takes in an integer x and adds it to the total property.

Then we have the second part:

[System.Runtime.CompilerServices.NullableContext(1)]
public static int GetTotalOfIntListWithForEachMethod(List<int> prices)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.total = 0;
    
    prices.ForEach(new Action<int>(<>c__DisplayClass0_.<GetTotalOfIntListWithForEachMethod>b__0));
    
    return <>c__DisplayClass0_.total;
}

This is the actual GetTotalOfIntListWithForEachMethod() method. It has the [System.Runtime.CompilerServices.NullableContext(1)] attribute, which informs the compiler that nullable context annotations are enabled. In the method, we utilize an instance of the <>c__DisplayClass0_0 class to keep track of the total.

Next, we initialize <>c__DisplayClass0_0.total and set it to zero. Then we use the ForEach() method on the prices list, with a lambda expression that we pass as an argument to calculate the total by invoking the <GetTotalOfIntListWithForEachMethod>b__0() method of the <>c__DisplayClass0_0 class.

Finally, we return the <>c__DisplayClass0_0.total.

Lowered C# Code for the Foreach Statement

We have only one part for the lowered version of the foreach statement:

[System.Runtime.CompilerServices.NullableContext(1)]
public static int GetTotalOfIntListWithForEachMethod(List<int> prices)
{
    int num = 0;
    List<int>.Enumerator enumerator = prices.GetEnumerator();
    
    try
    {
        while (enumerator.MoveNext())
        {
            int current = enumerator.Current;
            num += current;
        }
    }
    finally
    {
        ((IDisposable)enumerator).Dispose();
    }
    
    return num;
}

We have the GetTotalOfIntListWithForEachMethod(), again annotated to inform the compiler that nullable context annotations are enabled. Next, we initialize an integer variable num and set it to zero.

Next, we use the GetEnumerator() method of the prices list to get its enumerator. Then we have a tryfinally block. In the try part we have a while statement that is exited when the MoveNext() method of the enumerator returns false. In the while loop itself, we add the value of the enumerator’s Current property to the num variable. Then, in the finally block we dispose of the enumerator variable. And, finally, we return the num variable.

Comparing the Performance of ForEach Method and Foreach Statement

As with any good comparison article, we won’t go without some benchmarks. We will use BenchmarkDotNet to check performance and memory allocation. We will compare the performance of both iteration methods with a list of integers as well as a list of custom class objects.

Preparation

Let’s define our class:

public class Product
{
    public Guid Id { get; set; }
    public int Price { get; set; }

    public Product(int price)
    {
        Id = Guid.NewGuid();
        Price = price;
    }
}

Also, in our Iterators class (you can find the source code here), we create versions of our previous methods that take in a List<Product> as a parameter and we use both iteration approaches to add the value of the Price property to the total value.

Then we move on to our benchmarking class:

[MemoryDiagnoser(true)]
public class IterationBenchmark
{
    private const int MinPrice = 10;
    private const int MaxPrice = 100;
    private readonly List<int> _prices = new();
    private readonly List<Product> _products = new();

    [GlobalSetup]
    public void Setup()
    {
        for (int i = 0; i < 10_000; i++)
        {
            var price = Random.Shared.Next(MinPrice, MaxPrice);

            _prices.Add(price);
            _products.Add(new Product(price));
        }
    }
}

We use the fields to define minimum and maximum prices, as well as to store a list of integers and products. In the Setup() method, we add 10,000 items to each list and use Random.Shared‘s Next() method to generate random prices.

Next, let’s add our benchmark methods:

[Benchmark]
public void GetTotalOfIntListWithForEachMethod()
{
    Iterators.GetTotalOfIntListWithForEachMethod(_prices);
}

[Benchmark]
public void GetTotalOfIntListWithForeachStatement()
{
    Iterators.GetTotalOfIntListWithForeachStatement(_prices);
}

[Benchmark]
public void GetTotalOfProductsListWithForEachMethod()
{
    Iterators.GetTotalOfProductsListWithForEachMethod(_products);
}

[Benchmark]
public void GetTotalOfProductsListWithForeachStatement()
{
    Iterators.GetTotalOfProductsListWithForeachStatement(_products);
}

Benchmark

Finally, let’s add a single line to our Program class:

BenchmarkRunner.Run<IterationBenchmark>();

After all the code, we can run our application in the Release mode and examine the results:

| Method                                     | Mean      | Error     | StdDev    | Allocated |
|------------------------------------------- |----------:|----------:|----------:|----------:|
| GetTotalOfIntListWithForeachStatement      | 5.740 us  | 0.0445 us | 0.0395 us |         - |
| GetTotalOfIntListWithForEachMethod         | 17.919 us | 0.1161 us | 0.1029 us |      88 B |
| GetTotalOfProductsListWithForeachStatement | 10.551 us | 0.2106 us | 0.4489 us |         - |
| GetTotalOfProductsListWithForEachMethod    | 19.038 us | 0.1516 us | 0.1418 us |      88 B |

The foreach statement outperforms the ForEach() method for both collection types. For simple integers, it is about three times faster and has no memory allocation. When it comes to List<Product> it is two times faster again with no memory allocation.

Despite the fact that the ForEach() method has direct access to the internal structure of the collection, which can mean better performance, it performs worse than the foreach statement. The use of a lambda expression and its requirement to allocate memory on the heap cancels the potential advantage. This memory allocation is the result of the <>c__DisplayClass0_0 class generation in the low-level C# code.

Conclusion

In conclusion, our exploration of iteration methods in C# sheds light on the differences between  foreach statement and the ForEach() method.

While both serve the purpose of iterating through collections, the foreach statement emerges as the performance champion, boasting faster execution and minimal memory allocation as well as the ability to hold complex logic. In contrast, the ForEach() method, while convenient for simple tasks, introduces some overhead due to its need to generate a separate class in low-level C# code. Ultimately, when choosing between these methods you should consider your specific use case and performance requirements but in scenarios where speed and resource efficiency are crucial, the foreach statement remains the go-to option.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!