In this article, we are going to look at different ways to remove elements from a generic list while iterating through it.
If we try simply to do a test and call Remove
when we want to discard an element, the solution would throw an exception:
private static readonly List<int> _numberList = Enumerable.Range(1, 20).ToList(); public static List<int> NaiveIterateRemove(List<int> sourceList) { foreach (var el in sourceList) { if (el % 2 == 0) { sourceList.Remove(el); } } return sourceList; }
Here, we define a simple List
of numbers in a range and attempt to remove the even elements as we loop through it. However, this fails with InvalidOperationException
.
We can get around this error in a number of ways depending on what feels clean to us (stylistically). We will also take a look at the comparative performance of the different methods.
Using For/Foreach to Remove Elements From a Generic List
The simplest solution is to do the same thing as in the naive example, only go backward with a for
loop. We can then safely remove elements because they are being removed from the “end” of the List
and do not affect the iteration:
public static List<int> ReverseIterate(List<int> sourceList) { for (int i = sourceList.Count - 1; i >= 0; i--) { if (sourceList[i] % 2 == 0) { sourceList.RemoveAt(i); } } return sourceList; }
Here we iterate backward over the List
and call RemoveAt
with the index of the element that we wish to remove. This works well and is easy to understand.
However, by simply making a copy of the List
using the ToList()
method, we can rather use foreach
method and iterate forward:
public static List<int> SimpleIterateRemoveWithToList(List<int> sourceList) { foreach (var el in sourceList.ToList()) { if (el % 2 == 0) { sourceList.Remove(el); } } return sourceList; }
Here we again iterate forward through the list using a foreach
loop. This is effectively identical code to the initial example with the exception that we are now iterating through a copy of the List
while removing from the original List
. If we saw this in the production code, we might not initially understand why we call ToList()
on an object that was already a List
.
Using RemoveAll to Remove Elements From a Generic List
We can also use the built-in RemoveAll
method with a predicate instead of using a loop. This is a simple, fast, and readable solution that modifies the original list:
sourceList.RemoveAll(el => el % 2 == 0);
The only problem with this is that we often need to perform some action on the elements during the iteration. Fortunately, we can still use RemoveAll
in this case by using a lambda function for the predicate:
public static List<int> OneLineRemoveAllWithSideEffect(List<int> sourceList) { sourceList.RemoveAll(item => { PerformOperation(item); return item % 2 == 0; }); ; return sourceList; }
Here we again use the RemoveAll
method, but we also use a lambda function as the argument rather than a simple predicate. The return value of this lambda function determines if the element is to be removed. We can use this form to operate on each element within the body of the lambda.
This provides us with a more functional style but is actually no simpler or more compact than a simple foreach
loop.
Testing Performance
We will have a quick look at the performance of these different methods to help us decide what might be best to use:
| Method | Mean | Error | StdDev | Allocated | |--------------------------------- |----------:|----------:|----------:|-------------:| | RunReverseIterate | 6.640 ms | 0.0504 ms | 0.0447 ms | 4 B | | RunSimpleIterateRemoveWithToList | 21.411 ms | 0.4044 ms | 0.4814 ms | 40,010,112 B | | RunOneLineRemoveAll | 13.119 ms | 0.0664 ms | 0.0554 ms | 8 B |
We get these results by running BenchmarkDotNet. For this benchmark, we are using a 200 000 integer array and performing 100 iterations of each method.
As a result, we can see that running a raw reverse for loop is easily the most efficient in time and memory. Note that “Allocated” memory measures are used by the method under test. Whereas the actual data used exists at a class level.
As we expect, using ToList
is very memory intensive due to making copies of the data per iteration.
Conclusion
Realistically, in many applications making copies of simple Lists is not really any cause for concern. But that’s not the case if we are dealing with massive arrays or large numbers of operations.
Ultimately, the final choice we make is really a combination of stylistic preferences and performance requirements. If we prefer a functional style of coding then using RemoveAll
may appear neater. If we prefer code that is easy to read, using ToList
with foreach
is appealing. And if we are writing a performance-critical application then using the reverse for
loop is ideal (although it may be beneficial to use a different data structure in this case).