In this article, we are going to cover six different ways to concatenate lists in C#. After learning about every approach, we are going to evaluate some benchmarks to find out which is the most efficient approach.
Let’s dive in.
Concatenate Lists in C# Using Add
Let’s create a UsingAdd
method to concatenate two lists:
public List<string> UsingAdd(List<string> firstList, List<string> secondList) { var result = new List<string>(firstList.Count() + secondList.Count()); foreach (var item in firstList) { result.Add(item); } foreach (var item in secondList) { result.Add(item); } return result; }
This method receives two lists as input parameters that we want to concatenate.
List works as a reference type, so if we modify it within our method, we will modify every reference. For this reason, changing one of the input lists is not a good practice. You can read more about lists in our article List Collection in C#.
That said, we instantiate a new string list to represent the result of our method. Also, we set up the list’s capacity to the sum of the two lists’ lengths we want to combine.
In the second step, we iterate through the firstList
and add each element to the result
list. Then, we repeat the process for the secondList
.
Once we have iterated through the two lists, we return the result
list containing the elements of both lists.
Concatenate Using Enumerable.Concat
We can use two extension methods in the Enumerable
static class to concatenate two list elements. The first one is Enumerable.Concat
:
public List<string> UsingEnumerableConcat(List<string> firstList, List<string> secondList) { return Enumerable.Concat(firstList, secondList).ToList(); }
We simply call the Concat
method from the Enumerable
static class. This method requires two IEnumerable
input parameters and returns a new IEnumerable
containing every element of the input lists.
After that, we call the ToList()
method and return the result.
Concatenate Lists in C# Using Enumerable.Union
The second method that we can use from the Enumerable
static class is the Union
method:
public List<string> UsingEnumerableUnion(List<string> firstList, List<string> secondList) { return Enumerable.Union(firstList, secondList).ToList(); }
The Union
method receives two parameters (firstList
and secondList
). After calling this method, it is necessary to call the ToList()
method to create a new list from the IEnumerable
and return it.
It is important to mention that Union
method removes duplicated elements from its result.
This method also provides an overloaded method with a third parameter representing an IEqualityComparer
to compare the elements within the two lists.
Concatenate Using AddRange
Let’s implement a UsingAddRange
method to concatenate two lists elements:
public List<string> UsingAddRange(List<string> firstList, List<string> secondlist) { var result = new List<string>(firstList.Count() + secondList.Count()); result.AddRange(firstList); result.AddRange(secondlist); return result; }
The UsingAddRange
method receives two lists as input parameters and returns a new list with the combined elements from the two lists.
First, we instantiate an object (result
) that is going to be our method’s return.
Then, we call the AddRange
method to add every element from the firstList
inside the result object and repeat the process with the secondList
.
Finally, we return the result
list with every element from both lists.
Similarly to the UsingAdd
method, it is not a good practice to call the AddRange
method against one of the input lists because we don’t want to change it.
Concatenate Using List.CopyTo
Let’s create a UsingCopyTo
method to concatenate two lists using an array:
public List<string> UsingCopyTo(List<string> firstList, List<string> secondList) { var combinedArray = new string[firstList.Count() + secondList.Count()]; firstList.CopyTo(combinedArray, 0); secondList.CopyTo(combinedArray, firstList.Count()); return combinedArray.ToList(); }
First, we instantiate a new array (combinedArray
) and set its length to the sum of both input lists’ lengths.
Then, we copy the first list of elements into the combinedArray
starting at index 0
.
After that, we repeat the process with the secondList
, but this time, the start index needs to be the firstList.Count()
(after the last element from the firstList
).
Finally, we call ToList()
to return a new list of strings based on the combinedArray
.
Concatenate Lists in C# Using SelectMany
Let’s concatenate two lists using SelectMany
method:
public List<string> UsingSelectMany(List<string> firstList, List<string> secondList) { var combinedArray = new[] { firstList, secondList}.SelectMany(x => x); return combinedArray.ToList(); }
This approach consists of instantiating a new array (combinedArray
) and filling it with the two input parameter lists (firstList
and secondList
).
Then we call the SelectMany
method to flatten the result into a single IEnumerable
. The SelectMany
method receives a Func
delegate to apply to each element of the combinedArray
.
Finally, we return a new list calling the ToList()
method on the combinedArray
.
Benchmark
In this article, we are going to evaluate benchmark results against each approach with lists of 50 thousand elements and lists of 1 million elements.
Let’s start by running the benchmark with two lists of 50 thousand elements:
private readonly List<string> _firstList = Enumerable.Repeat("Code", 50_000).ToList(); private readonly List<string> _secondList = Enumerable.Repeat("Maze", 50_000).ToList();
| Method | Mean | Error | StdDev | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------- |-----------:|---------:|----------:|-----:|---------:|---------:|---------:|------------:| | UsingAddRange | 589.3 us | 4.19 us | 51.47 us | 1 | 19.5313 | 19.5313 | 19.5313 | 800,196 B | | UsingEnumerableConcat | 596.2 us | 11.58 us | 15.85 us | 2 | 249.0234 | 249.0234 | 249.0234 | 800,140 B | | UsingSelectMany | 698.2 us | 16.16 us | 47.39 us | 3 | 8.7891 | 8.7891 | 8.7891 | 1,200,233 B | | UsingCopyTo | 1,097.7 us | 27.67 us | 78.48 us | 4 | 498.0469 | 498.0469 | 498.0469 | 1,600,248 B | | UsingAdd | 1,109.6 us | 13.40 us | 12.54 us | 5 | 248.0469 | 248.0469 | 248.0469 | 800,140 B | | UsingEnumerableUnion | 2,704.1 us | 28.86 us | 24.10 us | 6 | - | - | - | 402 B |
We can see that the UsingAddRange
method is the fastest approach, more than 4,5 times faster than the slowest approach (UsingEnumerableUnion
).
Let’s increase the number of elements to 1 million and run the benchmark:
| Method | Mean | Error | StdDev | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------- |---------:|---------:|---------:|-----:|---------:|---------:|---------:|----------:| | UsingAddRange | 10.74 ms | 0.207 ms | 0.212 ms | 1 | 125.0000 | 125.0000 | 125.0000 | 15,626 KB | | UsingEnumerableConcat | 10.89 ms | 0.195 ms | 0.314 ms | 2 | 125.0000 | 125.0000 | 125.0000 | 15,626 KB | | UsingSelectMany | 14.92 ms | 0.298 ms | 0.673 ms | 3 | 125.0000 | 125.0000 | 125.0000 | 23,438 KB | | UsingCopyTo | 18.93 ms | 0.433 ms | 0.844 ms | 4 | 187.5000 | 187.5000 | 187.5000 | 31,251 KB | | UsingAdd | 19.58 ms | 0.370 ms | 1.005 ms | 5 | 281.2500 | 281.2500 | 281.2500 | 15,625 KB | | UsingEnumerableUnion | 54.89 ms | 0.442 ms | 0.392 ms | 6 | - | - | - | 1 KB |
Now that we are running a much heavier benchmark, we can see that our result isn’t in us
(microsecond 0,000001 sec) anymore but in ms (0,001 sec).
The order of the approaches remains the same and the fastest approach is more than 5 times faster than the slowest approach.
Considering both benchmarks’ results (50 thousand elements and 1 million elements), the difference between the fastest and the second-fastest approach is irrelevant (0,7 us and 0,15 ms, respectively).
List Capacity Performance Impact
In C#, the List
generic class has three constructors. In one of them, it is possible to specify the list’s capacity, as we did in the UsingAdd
and UsingAddRange
methods.
For this benchmark, let’s remove the list’s capacity from the constructors and run the benchmark with the lists of 50 thousand elements:
| Method | Mean | Error | StdDev | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------- |-----------:|---------:|----------:|-----:|---------:|---------:|---------:|------------:| | UsingEnumerableConcat | 607.3 us | 11.96 us | 23.05 us | 1 | 249.0234 | 249.0234 | 249.0234 | 800,196 B | | UsingAddRange | 705.5 us | 17.58 us | 51.55 us | 2 | 15.6250 | 15.6250 | 15.6250 | 1,00,145 B | | UsingSelectMany | 797.5 us | 16.10 us | 47.22 us | 3 | 14.6484 | 14.6484 | 14.6484 | 1,200,256 B | | UsingCopyTo | 1,258.5 us | 24.89 us | 65.58 us | 4 | 498.0469 | 498.0469 | 498.0469 | 1,600,248 B | | UsingAdd | 1,693.1 us | 41.68 us | 120.91 us | 5 | 167.9688 | 105.4688 | 105.4688 | 2,745,462 B | | UsingEnumerableUnion | 2,675.8 us | 37.52 us | 31.33 us | 6 | - | - | - | 402 B |
Note that when we don’t set the list’s capacity, the UsingAddRange
loses the fastest position and becomes 14% slower than the UsingEnumerableConcat
approach. Also, it becomes 16% slower than when we set the list’s capacity.
Let’s evaluate the benchmark result with two lists of 1 million elements:
| Method | Mean | Error | StdDev | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------- |---------:|---------:|---------:|-----:|---------:|---------:|---------:|-------------:| | UsingEnumerableConcat | 11.06 ms | 0.218 ms | 0.478 ms | 1 | 125.0000 | 125.0000 | 125.0000 | 16,000,649 B | | UsingAddRange | 15.36 ms | 0.320 ms | 0.929 ms | 2 | 125.0000 | 125.0000 | 125.0000 | 24,000,632 B | | UsingSelectMany | 15.37 ms | 0.301 ms | 0.565 ms | 3 | 125.0000 | 125.0000 | 125.0000 | 24,000,645 B | | UsingCopyTo | 22.14 ms | 0.364 ms | 0.357 ms | 4 | 187.5000 | 187.5000 | 187.5000 | 32,000,915 B | | UsingAdd | 31.40 ms | 0.693 ms | 2.022 ms | 5 | 437.5000 | 375.0000 | 375.0000 | 33,555,977 B | | UsingEnumerableUnion | 54.36 ms | 0.872 ms | 0.816 ms | 6 | - | - | - | 527 B |
This time, the difference between UsingAddRange
and UsingEnumerableConcat
is even more significant since UsingEnumerableConcat
is 38% faster.
When using the predefined list’s capacity, the UsingAddRange
approach is 43% faster than when we are using the non-predefined list’s capacity.
The same applies to the UsingAdd
approach, which achieves more than a 60% increase in performance when we set up the list’s capacity.
The memory allocation increases from 15 MB to 22 MB in the UsingAddRange
approach and from 15 MB to 32 MB in the UsingAdd
approach when we compare the predefined and non-predefined list’s capacity benchmarks.
Conclusion
In this article, we have shown six different ways to concatenate lists in C#.
Also, we investigated the performance difference by running some benchmarks that showed us the fastest (UsingAddRange
) and the slowest (UsingEnumerableUnion
) approach. However, we can’t discard the Union
method, because, it can be useful in scenarios where we need to remove duplicates.
Finally, we have seen that sometimes, using the list’s constructor with capacity is very useful to make our code more efficient.