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.
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>)
.
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 try
–finally
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.