From 4b8e7ed8ba93e594f1f8db4704924b4abc5e7962 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sat, 10 Oct 2026 22:09:39 +0200 Subject: [PATCH] How to Merge Arrays in C#: fix two merge methods, add content tests, retarget to net10.0 MergeUsingBlockCopy passed element counts where Buffer.BlockCopy expects bytes, so it copied a quarter of each array (CA2018). MergeUsingArrayCopyWithResize resized an empty array instead of the first one, so the first array never reached the result. Both now merge correctly. Every test now checks the merged contents, not only the length; the old tests passed on both broken methods. The source arrays overlap by half so Union merges comparable work, and _arraySize is back to 1000 to match the article. Adds MergeUsingSpanCopyTo and MergeUsingCollectionExpression, renames the SelectMany lambda parameter, and moves both projects to net10.0 with current packages. --- .../HowToMergeArrays/HowToMergeArrays.csproj | 4 +- .../HowToMergeArrays/MergeArrayBenchmark.cs | 40 ++++++--- .../HowToMergeArrays/Tests/Tests.cs | 81 ++++++++++++++----- .../HowToMergeArrays/Tests/Tests.csproj | 10 +-- 4 files changed, 99 insertions(+), 36 deletions(-) diff --git a/collections-arrays/HowToMergeArrays/HowToMergeArrays/HowToMergeArrays.csproj b/collections-arrays/HowToMergeArrays/HowToMergeArrays/HowToMergeArrays.csproj index 79e7148fd0..62be4f867f 100644 --- a/collections-arrays/HowToMergeArrays/HowToMergeArrays/HowToMergeArrays.csproj +++ b/collections-arrays/HowToMergeArrays/HowToMergeArrays/HowToMergeArrays.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net10.0 enable enable - + diff --git a/collections-arrays/HowToMergeArrays/HowToMergeArrays/MergeArrayBenchmark.cs b/collections-arrays/HowToMergeArrays/HowToMergeArrays/MergeArrayBenchmark.cs index 4fb2d0ba59..61bb39b2f2 100644 --- a/collections-arrays/HowToMergeArrays/HowToMergeArrays/MergeArrayBenchmark.cs +++ b/collections-arrays/HowToMergeArrays/HowToMergeArrays/MergeArrayBenchmark.cs @@ -6,7 +6,7 @@ namespace HowToMergeArrays [MemoryDiagnoser, Orderer(SummaryOrderPolicy.FastestToSlowest)] public class MergeArrayBenchmark { - private static readonly int _arraySize = 50000; + private static readonly int _arraySize = 1000; public int ArraySize { @@ -29,12 +29,12 @@ public int[] MergeUsingArrayCopyWithNewArray(int[] firstArray, int[] secondArray public int[] MergeUsingArrayCopyWithResize(int[] firstArray, int[] secondArray) { var originalFirstArrayLength = firstArray.Length; - int[] firstTempArray = Array.Empty(); + var combinedArray = firstArray; - Array.Resize(ref firstTempArray, firstArray.Length + secondArray.Length); - Array.Copy(secondArray, 0, firstTempArray, originalFirstArrayLength, secondArray.Length); + Array.Resize(ref combinedArray, firstArray.Length + secondArray.Length); + Array.Copy(secondArray, 0, combinedArray, originalFirstArrayLength, secondArray.Length); - return firstTempArray; + return combinedArray; } [Benchmark] @@ -48,6 +48,17 @@ public int[] MergeUsingArrayCopyTo(int[] firstArray, int[] secondArray) return combinedArray; } + [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; + } + [Benchmark] [ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))] public int[] MergeUsingLinqConcat(int[] firstArray, int[] secondArray) @@ -71,7 +82,16 @@ public int[] MergeUsingLinqUnion(int[] firstArray, int[] secondArray) public int[] MergeUsingLinqSelectMany(int[] firstArray, int[] secondArray) { var firstAndSecondArray = new[] { firstArray, secondArray }; - var combinedArray = firstAndSecondArray.SelectMany(animal => animal).ToArray(); + var combinedArray = firstAndSecondArray.SelectMany(array => array).ToArray(); + + return combinedArray; + } + + [Benchmark] + [ArgumentsSource(nameof(GetSourceArrayPopulatedWithNumbers))] + public int[] MergeUsingCollectionExpression(int[] firstArray, int[] secondArray) + { + int[] combinedArray = [.. firstArray, .. secondArray]; return combinedArray; } @@ -82,8 +102,8 @@ public int[] MergeUsingBlockCopy(int[] firstArray, int[] secondArray) { var combinedArray = new int[firstArray.Length + secondArray.Length]; - Buffer.BlockCopy(firstArray, 0, combinedArray, 0, firstArray.Length); - Buffer.BlockCopy(secondArray, 0, combinedArray, 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; } @@ -112,8 +132,8 @@ public int[] MergeUsingNewArrayManually(int[] firstArray, int[] secondArray) public IEnumerable GetSourceArrayPopulatedWithNumbers() { - var first = Enumerable.Repeat(1, _arraySize).ToArray(); - var second = Enumerable.Repeat(2, _arraySize).ToArray(); + var first = Enumerable.Range(0, _arraySize).ToArray(); + var second = Enumerable.Range(_arraySize / 2, _arraySize).ToArray(); yield return new object[] { first, second }; } diff --git a/collections-arrays/HowToMergeArrays/Tests/Tests.cs b/collections-arrays/HowToMergeArrays/Tests/Tests.cs index 46f17378b6..0a45b180df 100644 --- a/collections-arrays/HowToMergeArrays/Tests/Tests.cs +++ b/collections-arrays/HowToMergeArrays/Tests/Tests.cs @@ -1,4 +1,5 @@ using HowToMergeArrays; +using System.Collections.Generic; using System.Linq; using Xunit; @@ -8,89 +9,131 @@ public class Tests { private readonly MergeArrayBenchmark _mergeArrayRunner; private readonly int _combinedExpectedSize; - private readonly int _combinedExpectedSizeDistinct = 2; + private readonly int _combinedExpectedSizeDistinct; private readonly int[] first; private readonly int[] second; + private readonly int[] _expectedMerged; + private readonly int[] _expectedDistinct; public Tests() { _mergeArrayRunner = new MergeArrayBenchmark(); + var source = _mergeArrayRunner.GetSourceArrayPopulatedWithNumbers().Single(); + first = (int[])source[0]; + second = (int[])source[1]; + _combinedExpectedSize = _mergeArrayRunner.ArraySize * 2; - first = Enumerable.Repeat(1, _mergeArrayRunner.ArraySize).ToArray(); - second =Enumerable.Repeat(2, _mergeArrayRunner.ArraySize).ToArray(); + _combinedExpectedSizeDistinct = _mergeArrayRunner.ArraySize + _mergeArrayRunner.ArraySize / 2; + + // The expected results are built without any of the methods under test: + // every element of the first array, then every element of the second. + var expected = new List(first); + expected.AddRange(second); + _expectedMerged = expected.ToArray(); + + // The second array starts halfway through the first, so the distinct values run + // from 0 to one and a half times the array size, in first-seen order. + _expectedDistinct = Enumerable.Range(0, _combinedExpectedSizeDistinct).ToArray(); } [Fact] - public void WhenMergingWithArrayCopyAndNewArray_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithArrayCopyAndNewArray_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingArrayCopyWithNewArray(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } [Fact] - public void WhenMergingWithArrayCopyAndResize_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithArrayCopyAndResize_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingArrayCopyWithResize(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } [Fact] - public void WhenMergingWithArrayCopyTo_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithArrayCopyAndResize_ThenFirstArrayIsLeftUnchanged() + { + var combinedArray = _mergeArrayRunner.MergeUsingArrayCopyWithResize(first, second); + + Assert.NotSame(first, combinedArray); + Assert.Equal(_mergeArrayRunner.ArraySize, first.Length); + } + + [Fact] + public void WhenMergingWithArrayCopyTo_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingArrayCopyTo(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } [Fact] - public void WhenMergingWithLinqConcat_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithSpanCopyTo_ThenCombinedArrayHoldsFirstThenSecond() + { + var combinedArray = _mergeArrayRunner.MergeUsingSpanCopyTo(first, second); + + Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); + } + + [Fact] + public void WhenMergingWithLinqConcat_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingLinqConcat(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } [Fact] - public void WhenMergingWithLinqUnion_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSizeDistinct() + public void WhenMergingWithLinqUnion_ThenCombinedArrayHoldsEachDistinctValueOnce() { var combinedArray = _mergeArrayRunner.MergeUsingLinqUnion(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSizeDistinct, combinedArray.Length); + Assert.Equal(_expectedDistinct, combinedArray); } [Fact] - public void WhenMergingWithLinqSelectMany_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithLinqSelectMany_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingLinqSelectMany(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); + } + + [Fact] + public void WhenMergingWithCollectionExpression_ThenCombinedArrayHoldsFirstThenSecond() + { + var combinedArray = _mergeArrayRunner.MergeUsingCollectionExpression(first, second); + + Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } [Fact] - public void WhenMergingWithBlockCopy_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithBlockCopy_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingBlockCopy(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } [Fact] - public void WhenMergingWithNewArrayManually_ThenCombinedArrayNotEmptyAndLengthEqualsDoubleSize() + public void WhenMergingWithNewArrayManually_ThenCombinedArrayHoldsFirstThenSecond() { var combinedArray = _mergeArrayRunner.MergeUsingNewArrayManually(first, second); - Assert.NotEmpty(combinedArray); Assert.Equal(_combinedExpectedSize, combinedArray.Length); + Assert.Equal(_expectedMerged, combinedArray); } } } diff --git a/collections-arrays/HowToMergeArrays/Tests/Tests.csproj b/collections-arrays/HowToMergeArrays/Tests/Tests.csproj index 1026b9d8dd..0cb7d55ae9 100644 --- a/collections-arrays/HowToMergeArrays/Tests/Tests.csproj +++ b/collections-arrays/HowToMergeArrays/Tests/Tests.csproj @@ -1,20 +1,20 @@ - net6.0 + net10.0 enable false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all