Updated on

Merging two arrays in C# means allocating a third array the size of both and copying each source into its own part of it. Since C# 12 that is one line: int[] merged = [..firstArray, ..secondArray];.

The older techniques spell those two steps out. Array.Copy, CopyTo and Buffer.BlockCopy copy blocks of memory, LINQ’s Concat defers the copy and builds the array at the end, and a for loop moves one element at a time, which makes it the slowest of them.

The one exception is Union. It removes duplicates, so it returns a shorter array whenever the two inputs share values, and we include it here for comparison.

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

Prepare the Environment

First, let’s add the MergeArrayBenchmark class, which we will use to compare different ways of merging arrays.

To start with, let’s create a private field to set the default length of an array:

public class MergeArrayBenchmark
{
    private static readonly int _arraySize = 1000;
}

Here, we create the _arraySize variable with the initial value set to one thousand.

Then, let’s add a public method that supplies the source arrays to the benchmark:

public IEnumerable<object[]> GetSourceArrayPopulatedWithNumbers()
{
    var first = Enumerable.Range(0, _arraySize).ToArray();
    var second = Enumerable.Range(_arraySize / 2, _arraySize).ToArray();

    yield return new object[] { first, second };
}

The static Range method from the Enumerable class generates a sequence of consecutive integers. The second array starts halfway through the first, so half of the values in each array also appear in the other. Every merge in this article keeps those duplicates except Union, which drops them.

Lastly, let’s introduce the BenchmarkDotNet library to get the benchmark results. We add the package to our console project:

dotnet add package BenchmarkDotNet

We are going to add a MemoryDiagnoser annotation to the MergeArrayBenchmark class to get memory allocation results for each merging method:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;

[MemoryDiagnoser, Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class MergeArrayBenchmark

We also use the Orderer to order the results from fastest to slowest.

How Do We Merge Two Arrays in C#?

Merging two arrays produces a third array holding the elements of the first followed by the elements of the second. Every technique in this article except Union does exactly that, and they differ mainly in how fast they get there.

The shortest is a collection expression, available since C# 12: int[] merged = [..firstArray, ..secondArray];. The compiler allocates the destination and copies both sources into it.

Array.Copy, CopyTo and Buffer.BlockCopy are the same two steps written by hand. We allocate a destination of the combined length, then copy each source into its own part of it.

LINQ’s Concat returns a lazy sequence that yields the first array then the second, so nothing is copied until ToArray materializes the result.

Union does something slightly different. It concatenates and then removes duplicates, so it returns fewer elements than it was given whenever the arrays share values.

A manual for loop merges correctly and is the slowest of the copying options, because the runtime copies a block of memory faster than our loop copies elements one by one.

To add a single value to an array instead of a whole second array, see our article on adding a single value to an existing array.

Using Array.Copy to Merge Arrays in C#

The first approach is the static Copy method from the Array class. This method copies a range of elements in one array to another array. Additionally, it can perform casting and boxing as required. With the Array.Copy method, we can define the source array, the initial index of the source array, the destination array, the initial index of the destination array, and the number of elements we want to copy. While copying from one array to another, the element types have to be compatible: an int[] copies into another int[] or into a wider long[], but not into a string[].

To examine all the ways, we are going to try two different methods of merging arrays using the Copy method. Our article on copying elements into a new array covers copying a single array.

Array.Copy with New Array

In the first case, let’s use the Copy method and copy the data from both arrays to the new destination array:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingArrayCopyWithNewArray(int[] firstArray, int[] secondArray)
{
    var combinedArray = new int[firstArray.Length + secondArray.Length];
    Array.Copy(firstArray, combinedArray, firstArray.Length);
    Array.Copy(secondArray, 0, combinedArray, firstArray.Length, secondArray.Length);

    return combinedArray;
}

We use the ArgumentsSource attribute to populate the firstArray and the secondArray with generated values.

Then, we create a new combinedArray variable using the total size of both initial arrays. Next, we use the Array.Copy method to copy all of the data from the firstArray to the combinedArray variable as the destination array. Additionally, we pass the length of the source array to copy the data.

After that, we use the same method to copy the data from the secondArray to the combinedArray. The only difference is that we specify the source and destination index. The source index is zero, and the destination index is firstArray.Length to add the data after the last element in the array.

Note that we add the Benchmark annotation from the BenchmarkDotNet library to measure the performance when the method is run.

Array.Copy with Array.Resize

Secondly, let’s try another way of using the Copy method, after Array.Resize gives us a larger copy of the first array:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingArrayCopyWithResize(int[] firstArray, int[] secondArray)
{
    var originalFirstArrayLength = firstArray.Length;
    var combinedArray = firstArray;

    Array.Resize(ref combinedArray, firstArray.Length + secondArray.Length);
    Array.Copy(secondArray, 0, combinedArray, originalFirstArrayLength, secondArray.Length);

    return combinedArray;
}

In the first step, we store the original length of the firstArray into the originalFirstArrayLength variable. Next, we point combinedArray at the firstArray and resize it, passing its reference and the total length of both arrays (first and second array) to the Resize static method from the Array class. After that, we use the Copy method to specify the source array, the source index, the destination array, the destination index right after the original elements, and the number of elements to copy.

Array.Resize does not grow the array in place, which is why it takes the array by ref. It allocates a new array of the new size, copies the old elements into it, and points the variable we passed at the new array. So combinedArray ends up holding the first array’s elements followed by empty slots for the second, while firstArray still refers to the original, unchanged array.

Merging Arrays Using CopyTo

This approach is similar to the method from the previous section. Here we don’t have to use the static method to perform the merge operation. That said, the CopyTo being an instance method is the only difference between the two.

Let’s see how we can use the CopyTo method to merge arrays:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingArrayCopyTo(int[] firstArray, int[] secondArray)
{
    var combinedArray = new int[firstArray.Length + secondArray.Length];
    firstArray.CopyTo(combinedArray, 0);
    secondArray.CopyTo(combinedArray, firstArray.Length);

    return combinedArray;
}

Again, we create the combinedArray variable as the destination array. Next, we use the CopyTo method directly from our firstArray object to copy the data into the combinedArray variable with the index set to zero.

Finally, we perform the same functionality on the secondArray. The only difference is that we set the index to the size of the firstArray. With that, we add the secondArray records after the existing data.

The same copy works through Span<T>, where combinedArray.AsSpan(firstArray.Length) names the destination slice instead of a start index:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingSpanCopyTo(int[] firstArray, int[] secondArray)
{
    var combinedArray = new int[firstArray.Length + secondArray.Length];
    firstArray.AsSpan().CopyTo(combinedArray);
    secondArray.AsSpan().CopyTo(combinedArray.AsSpan(firstArray.Length));

    return combinedArray;
}

Merge Arrays in C# Using LINQ Methods

In this section, we will use different LINQ methods to merge arrays. For lists instead of arrays, see Concatenate Lists in C#: How to Combine Two Lists.

We can use all the LINQ methods that we mention in this article from the static Enumerable class. However, we will use extension methods because they are arguably more readable and chain easily when we merge more than two sequences.

For example, we can use the static Concat method:

Enumerable.Concat(firstArray, secondArray).ToArray();

And the extension Concat method:

firstArray.Concat(secondArray).ToArray();

Merging Arrays with LINQ’s Concat

The first LINQ method is the Concat method. We can use this method to append two sequences or collections to return a new sequence. In our case, we will use it to concatenate two arrays. The Concat method allows the usage of duplicate elements in arrays and returns the items from the first sequence followed by the elements from the second sequence.

Like other LINQ methods in this article, this method is implemented by using deferred execution. Deferred execution in LINQ means that we can delay the evaluation of an expression until we require the actual value. With that, we can improve the performance of our app.

Let’s see the actual implementation of the Concat method when merging two arrays:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingLinqConcat(int[] firstArray, int[] secondArray)
{
    var combinedArray = firstArray.Concat(secondArray).ToArray();

    return combinedArray;
}

In this scenario, we create the combinedArray variable by simply using the Concat method, where we append the secondArray to the firstArray. We call ToArray on the result because the Concat method returns an IEnumerable<int>, not an array.

Using LINQ’s Union to Merge Arrays

Next up is the Union method from LINQ. We can use the Union method to combine the multiple data sources into one. This method removes all duplicate elements from the data sources. To decide what counts as a duplicate, it uses the default equality comparer, which compares an int by value, and an overload takes an IEqualityComparer<T> when we need different rules.

Union is basically Concat followed by Distinct, so we can expect the Union method to take more time to execute. Our article on removing duplicates from an array covers that job on its own.

Now, let’s combine both arrays by eliminating the duplicate elements:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingLinqUnion(int[] firstArray, int[] secondArray)
{
    var combinedArray = firstArray.Union(secondArray).ToArray();

    return combinedArray;
}

Similar to the previous example, we initialize the new array and assign it to the combinedArray variable with the Union method used on the firstArray to append the data from the secondArray and eliminate the duplicates. With our source arrays of 1,000 elements, half of the second array repeats values from the first, so Union returns 1,500 elements where every other method returns 2,000.

Merging Arrays With LINQ SelectMany

The last method from LINQ is the SelectMany method. This method projects each element of a sequence to an IEnumerable and flattens the resulting sequences into one sequence. Based on that, the SelectMany method combines the records from all arrays into one sequence.

We need to do a little more work when using the SelectMany method:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingLinqSelectMany(int[] firstArray, int[] secondArray)
{
    var firstAndSecondArray = new[] { firstArray, secondArray };
    var combinedArray = firstAndSecondArray.SelectMany(array => array).ToArray();

    return combinedArray;
}

First, we create the firstAndSecondArray variable as a jagged array, an int[][] whose elements are our two arrays. Then, we call the SelectMany method from the firstAndSecondArray and select the objects before we convert the collection into the combinedArray variable.

How Do We Merge Arrays With a Collection Expression in C#?

A collection expression merges arrays with the spread element, two dots before each source: int[] merged = [..firstArray, ..secondArray];.

The syntax arrived with C# 12 and works for any number of sources, so three arrays are [..first, ..second, ..third], and single elements mix in freely: [0, ..firstArray, ..secondArray, -1].

The compiler decides how to build the result. For arrays of a known length it allocates the destination once at the combined length and copies each source into it, which is the same work the Array.Copy version does by hand.

The target type matters too. With var in place of int[], the code does not compile, because the compiler needs a target type to know what to build. Declaring int[] merged is what makes the expression an array.

We reach for this version first. The whole merge fits on one line, and with no index arithmetic to write, there is no off-by-one error to make.

The spread syntax is one use of a general language feature, which our article on collection expressions and the spread element covers in full. In our benchmark class, the merge is a single statement:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingCollectionExpression(int[] firstArray, int[] secondArray)
{
    int[] combinedArray = [.. firstArray, .. secondArray];

    return combinedArray;
}

Why Does Buffer.BlockCopy Count Bytes and Not Elements?

Buffer.BlockCopy works in bytes, not elements. Microsoft Learn documents srcOffset and dstOffset as byte offsets, and count as the number of bytes to copy.

That makes it the one method here where the array’s Length is the wrong argument. An int is four bytes, so passing firstArray.Length copies a quarter of the array, and passing it as the destination offset lands an eighth of the way into the destination. Nothing throws, the result has the right length, and three quarters of it is zero.

The correct call multiplies by the element size: Buffer.BlockCopy(firstArray, 0, combinedArray, 0, firstArray.Length * sizeof(int)).

The .NET SDK warns about this mistake. Code analysis rule CA2018 fires when an Array.Length is the count for an array whose elements are larger than one byte.

The method also accepts only arrays of primitive types, such as int, double or byte, or of enums. Any other element type throws an ArgumentException at runtime, and that includes structs such as DateTime.

The two bands below show the same destination array under both calls: the top one is what the element count fills.

Two bands over a 2,000-slot array: passing an element count to Buffer.BlockCopy fills only the first 500 slots, while passing a byte count fills all 2,000.

Here is the corrected method, with every count and offset multiplied by sizeof(int):

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingBlockCopy(int[] firstArray, int[] secondArray)
{
    var combinedArray = new int[firstArray.Length + secondArray.Length];

    Buffer.BlockCopy(firstArray, 0, combinedArray, 0, firstArray.Length * sizeof(int));
    Buffer.BlockCopy(secondArray, 0, combinedArray, firstArray.Length * sizeof(int), secondArray.Length * sizeof(int));

    return combinedArray;
}

As in the previous examples, we use the firstArray and the secondArray variables, containing the objects of type int.

However, we cannot use the BlockCopy method with the reference types in C#, only with the primitives and enums.

The Buffer.BlockCopy reference on Microsoft Learn says: “This method copies count bytes from src, beginning at srcOffset, to dst, beginning at dstOffset.”

The BlockCopy method accepts five different parameters in this case:

  • Source array
  • Source offset
  • Destination array
  • Destination offset
  • Number of bytes

Merge Arrays in C# Manually

We have the option to implement the merging of arrays manually. In this case, we will spend more time on implementation, and the source code will be longer. On the other hand, sometimes manually implemented functionalities can work faster since they are often adapted to our use case.

That said, let’s implement a manual way of merging arrays by adding a new combined array:

[Benchmark]
[ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))]
public int[] MergeUsingNewArrayManually(int[] firstArray, int[] secondArray)
{
    var combinedArray = new int[firstArray.Length + secondArray.Length];
    for (int i = 0; i < combinedArray.Length; i++)
    {
        if (i >= firstArray.Length)
        {
            combinedArray[i] = secondArray[i - firstArray.Length];
        }
        else
        {
            combinedArray[i] = firstArray[i];
        }
    }

    return combinedArray;
}

In the MergeUsingNewArrayManually method, we iterate through initial arrays and add elements to the destination array using the index. First, we determine the total length of the destination array and create the combinedArray variable. Then, we use a for loop and retrieve specific elements using the index. Therefore, we assign the retrieved element according to the index in the destination array.

Merge Arrays in C# Benchmark Comparison

Finally, we are going to call the benchmark Run method from the Program class:

using BenchmarkDotNet.Running;

BenchmarkRunner.Run<MergeArrayBenchmark>();

Benchmark Comparison with 1000 Records

Now, we can check the benchmark results with the 1000 records in each array.

So, let’s run the project in the release configuration with the dotnet run -c Release command, and get the results:

BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

| Method                          | firstArray  | secondArray | Mean        | Error     | StdDev    | Median      | Gen0   | Gen1   | Allocated |
|-------------------------------- |------------ |------------ |------------:|----------:|----------:|------------:|-------:|-------:|----------:|
| MergeUsingCollectionExpression  | Int32[1000] | Int32[1000] |    311.0 ns |   6.19 ns |  11.79 ns |    306.7 ns | 0.9584 |      - |   7.84 KB |
| MergeUsingSpanCopyTo            | Int32[1000] | Int32[1000] |    317.5 ns |   6.39 ns |  16.73 ns |    312.2 ns | 0.9584 |      - |   7.84 KB |
| MergeUsingArrayCopyTo           | Int32[1000] | Int32[1000] |    318.0 ns |   6.41 ns |  17.43 ns |    310.1 ns | 0.9584 |      - |   7.84 KB |
| MergeUsingArrayCopyWithNewArray | Int32[1000] | Int32[1000] |    318.1 ns |   6.36 ns |  16.53 ns |    310.4 ns | 0.9584 |      - |   7.84 KB |
| MergeUsingArrayCopyWithResize   | Int32[1000] | Int32[1000] |    318.2 ns |   6.11 ns |  16.19 ns |    310.4 ns | 0.9584 |      - |   7.84 KB |
| MergeUsingBlockCopy             | Int32[1000] | Int32[1000] |    328.7 ns |   5.98 ns |  15.00 ns |    323.3 ns | 0.9584 |      - |   7.84 KB |
| MergeUsingLinqConcat            | Int32[1000] | Int32[1000] |    347.8 ns |   7.02 ns |  18.50 ns |    347.7 ns | 0.9661 |      - |   7.89 KB |
| MergeUsingLinqSelectMany        | Int32[1000] | Int32[1000] |    566.4 ns |  10.84 ns |  11.13 ns |    565.6 ns | 0.9756 |      - |   7.97 KB |
| MergeUsingNewArrayManually      | Int32[1000] | Int32[1000] |  1,582.0 ns |  28.94 ns |  55.77 ns |  1,575.4 ns | 0.9575 |      - |   7.84 KB |
| MergeUsingLinqUnion             | Int32[1000] | Int32[1000] | 18,976.9 ns | 317.27 ns | 296.78 ns | 18,874.0 ns | 7.7515 | 0.8545 |   63.3 KB |

The fastest methods for merging arrays of integers are the ones that copy whole blocks of memory, and they finish level. MergeUsingCollectionExpression, MergeUsingSpanCopyTo, MergeUsingArrayCopyTo, MergeUsingArrayCopyWithNewArray and MergeUsingArrayCopyWithResize land between 311.0 ns and 318.2 ns, with standard deviations of 12 to 17 ns, so their order is noise. The corrected MergeUsingBlockCopy takes 328.7 ns, and its error bar overlaps the Array.Copy ones, so Buffer.BlockCopy and Array.Copy tie. The Buffer.BlockCopy method still works only with primitive data types and enums.

After that, we have the two LINQ approaches that keep duplicates: Concat at 347.8 ns, close behind the copy methods, and SelectMany at 566.4 ns.

At last, we have our manually implemented method with 1,582.0 nanoseconds and the MergeUsingLinqUnion method with 18,976.9 nanoseconds, which is about 19 microseconds.

Memory allocation is 7.84 KB for every method that builds the array directly, the size of the 2,000 merged integers, and LINQ’s Concat and SelectMany add under 150 bytes on top. The MergeUsingLinqUnion method allocates 63.3 KB, eight times as much, because it builds a hash set of the values it has already seen.

Benchmark Comparison with 50,000 Records

We should check the performance of the methods with different sizes of arrays, so let’s populate the arrays with 50,000 records.

To do that, let’s simply modify our _arraySize variable in the MergeArrayBenchmark class:

private static readonly int _arraySize = 50000;

Now, let’s run the program again, and check the benchmark results:

BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

| Method                          | firstArray   | secondArray  | Mean        | Error     | StdDev    | Median      | Gen0     | Gen1     | Gen2     | Allocated  |
|-------------------------------- |------------- |------------- |------------:|----------:|----------:|------------:|---------:|---------:|---------:|-----------:|
| MergeUsingLinqSelectMany        | Int32[50000] | Int32[50000] |    41.78 us |  1.345 us |  3.964 us |    43.23 us |  84.4116 |  84.2285 |  84.2285 |  391.44 KB |
| MergeUsingArrayCopyTo           | Int32[50000] | Int32[50000] |   127.80 us |  2.492 us |  2.667 us |   127.24 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingSpanCopyTo            | Int32[50000] | Int32[50000] |   127.97 us |  2.503 us |  3.074 us |   127.81 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingCollectionExpression  | Int32[50000] | Int32[50000] |   128.32 us |  2.512 us |  3.176 us |   128.29 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingArrayCopyWithNewArray | Int32[50000] | Int32[50000] |   128.95 us |  2.575 us |  3.610 us |   129.15 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingBlockCopy             | Int32[50000] | Int32[50000] |   130.59 us |  2.551 us |  3.658 us |   128.88 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingArrayCopyWithResize   | Int32[50000] | Int32[50000] |   131.15 us |  2.549 us |  3.736 us |   130.94 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingLinqConcat            | Int32[50000] | Int32[50000] |   131.92 us |  2.591 us |  4.802 us |   131.51 us | 124.7559 | 124.7559 | 124.7559 |  390.74 KB |
| MergeUsingNewArrayManually      | Int32[50000] | Int32[50000] |   171.95 us |  1.287 us |  1.204 us |   171.90 us | 124.7559 | 124.7559 | 124.7559 |  390.69 KB |
| MergeUsingLinqUnion             | Int32[50000] | Int32[50000] | 1,098.27 us | 21.930 us | 19.441 us | 1,100.99 us | 693.3594 | 667.9688 | 666.0156 | 2566.06 KB |

In the example with 50,000 records, the top of the table changes. MergeUsingLinqSelectMany comes first at 41.78 microseconds, about three times faster than the copy methods, although it was slower than every one of them at 1,000 records. It allocates the same 391 KB as they do, and its Gen0 to Gen2 columns show fewer garbage collections per 1,000 operations, 84 against 125. We did not trace the reason, so we would measure our own workload before choosing SelectMany for speed.

Behind it, the methods that copy blocks of memory finish level again, between 127.80 and 131.92 microseconds with overlapping error bars, and LINQ’s Concat sits among them. The corrected Buffer.BlockCopy at 130.59 microseconds still ties with Array.Copy at 128.95 microseconds. The manual loop follows at 171.95 microseconds, and MergeUsingLinqUnion is last at 1,098.27 microseconds.

Again, memory usage is the same 390.69 KB for every method that builds the array directly, while the Union method allocates 2,566.06 KB, more than six times as much, for its hash set.

Which Way of Merging Arrays Should We Choose?

Reach for the collection expression first. int[] merged = [..firstArray, ..secondArray]; states the intent in one line and handles any number of sources, and the compiler emits the same allocate-and-copy work the hand-written versions do.

Use Array.Copy, CopyTo or Span<T>.CopyTo when the destination already exists, or when we are filling one slice of a larger buffer. Unlike the collection expression and LINQ, they let us choose where the copy lands.

Use Buffer.BlockCopy only for arrays of primitives or enums, and only when a measurement says it beats Array.Copy on our data. The price is the sizeof arithmetic in every call.

Use LINQ’s Concat when the merge is one step in a longer query and its result goes straight into another operator.

Use Union when duplicates must go, and expect it to be between 8 and 60 times slower than a plain copy in our benchmark, because it hashes every element.

Write the manual loop only when the merge is not a straight concatenation, such as interleaving two arrays.

Here is every technique from this article side by side:

TechniqueCodeKeeps duplicatesWorks withUse it when
Collection expression[..first, ..second]YesAny element typeDefault choice, C# 12 and later
Array.CopyArray.Copy(first, dest, first.Length)YesAny element typeThe destination already exists
CopyTofirst.CopyTo(dest, 0)YesAny element typeSame as above, instance-method style
Span<T>.CopyTofirst.AsSpan().CopyTo(dest)YesAny element typeMerging into a slice or a stack buffer
Buffer.BlockCopyBuffer.BlockCopy(first, 0, dest, 0, first.Length * sizeof(int))YesPrimitives and enumsByte-level work, count in bytes
LINQ Concatfirst.Concat(second).ToArray()YesAny element typeThe merge feeds a longer query
LINQ Unionfirst.Union(second).ToArray()NoAny element typeDuplicates must go
LINQ SelectManynew[] { first, second }.SelectMany(a => a).ToArray()YesAny element typeFlattening many arrays at once
Manual for loopfor (var i = 0; i < dest.Length; i++)YesAny element typeThe merge is not a plain concatenation
Array.ResizeArray.Resize(ref first, total) then copyYesAny element typeThe first array's variable should hold the result

The Code column abbreviates each call, and the sections above show every method in full.

Conclusion

In this article, we’ve learned how to implement different ways to merge arrays in C#. We have seen that there are many different ways to merge arrays. With different methods, we got different results. In the case of merging primitive data types in C#, the methods that copy whole blocks of memory proved to be the fastest at 1,000 elements, and Buffer.BlockCopy only ties with Array.Copy once its count is in bytes. On the other hand, with reference data types, we can use the Array.Copy method, or the CopyTo instance method if we don’t prefer static methods. Since C# 12, a collection expression does the same work in one line, and it is the version we reach for first.

LINQ methods are also very efficient, except for the Union method, which is expectedly slower due to duplicate removal.

Finally, we can use manually implemented methods, which turned out to be slower, but everything depends on the use case.

Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.