diff --git a/src/Common/src/System/Collections/Generic/LargeArrayBuilder.cs b/src/Common/src/System/Collections/Generic/LargeArrayBuilder.cs index fd17c34b1053..b91a202922bd 100644 --- a/src/Common/src/System/Collections/Generic/LargeArrayBuilder.cs +++ b/src/Common/src/System/Collections/Generic/LargeArrayBuilder.cs @@ -42,6 +42,20 @@ internal CopyPosition(int row, int column) /// internal int Column { get; } + /// + /// If this position is at the end of the current buffer, returns the position + /// at the start of the next buffer. Otherwise, returns this position. + /// + /// The length of the current buffer. + public CopyPosition Normalize(int endColumn) + { + Debug.Assert(Column <= endColumn); + + return Column == endColumn ? + new CopyPosition(Row + 1, 0) : + this; + } + /// /// Gets a string suitable for display in the debugger. /// @@ -197,9 +211,10 @@ public void CopyTo(T[] array, int arrayIndex, int count) /// The position in this builder that was copied up to. public CopyPosition CopyTo(CopyPosition position, T[] array, int arrayIndex, int count) { + Debug.Assert(array != null); Debug.Assert(arrayIndex >= 0); - Debug.Assert(count >= 0 && count <= Count); - Debug.Assert(array?.Length - arrayIndex >= count); + Debug.Assert(count > 0 && count <= Count); + Debug.Assert(array.Length - arrayIndex >= count); // Go through each buffer, which contains one 'row' of items. // The index in each buffer is referred to as the 'column'. @@ -216,25 +231,35 @@ public CopyPosition CopyTo(CopyPosition position, T[] array, int arrayIndex, int int row = position.Row; int column = position.Column; - for (; count > 0; row++, column = 0) + T[] buffer = GetBuffer(row); + int copied = CopyToCore(buffer, column); + + if (count == 0) { - T[] buffer = GetBuffer(index: row); + return new CopyPosition(row, column + copied).Normalize(buffer.Length); + } - // During this iteration, copy until we satisfy `count` or reach the - // end of the current buffer. - int copyCount = Math.Min(buffer.Length, count); + do + { + buffer = GetBuffer(++row); + copied = CopyToCore(buffer, 0); + } while (count > 0); - if (copyCount > 0) - { - Array.Copy(buffer, column, array, arrayIndex, copyCount); + return new CopyPosition(row, copied).Normalize(buffer.Length); - arrayIndex += copyCount; - count -= copyCount; - column += copyCount; - } - } + int CopyToCore(T[] sourceBuffer, int sourceIndex) + { + Debug.Assert(sourceBuffer.Length > sourceIndex); + + // Copy until we satisfy `count` or reach the end of the current buffer. + int copyCount = Math.Min(sourceBuffer.Length - sourceIndex, count); + Array.Copy(sourceBuffer, sourceIndex, array, arrayIndex, copyCount); - return new CopyPosition(row: row, column: column); + arrayIndex += copyCount; + count -= copyCount; + + return copyCount; + } } /// diff --git a/src/Common/src/System/Collections/Generic/SparseArrayBuilder.cs b/src/Common/src/System/Collections/Generic/SparseArrayBuilder.cs index 57eeeb90b6e5..4829a9ce464c 100644 --- a/src/Common/src/System/Collections/Generic/SparseArrayBuilder.cs +++ b/src/Common/src/System/Collections/Generic/SparseArrayBuilder.cs @@ -113,9 +113,10 @@ public SparseArrayBuilder(bool initialize) /// The number of items to copy. public void CopyTo(T[] array, int arrayIndex, int count) { + Debug.Assert(array != null); Debug.Assert(arrayIndex >= 0); Debug.Assert(count >= 0 && count <= Count); - Debug.Assert(array?.Length - arrayIndex >= count); + Debug.Assert(array.Length - arrayIndex >= count); int copied = 0; var position = CopyPosition.Start; @@ -149,8 +150,11 @@ public void CopyTo(T[] array, int arrayIndex, int count) count -= reservedCount; } - // Finish copying after the final marker. - _builder.CopyTo(position, array, arrayIndex, count); + if (count > 0) + { + // Finish copying after the final marker. + _builder.CopyTo(position, array, arrayIndex, count); + } } /// diff --git a/src/System.Linq/tests/ConcatTests.cs b/src/System.Linq/tests/ConcatTests.cs index 89b9052942bc..425fefbdd736 100644 --- a/src/System.Linq/tests/ConcatTests.cs +++ b/src/System.Linq/tests/ConcatTests.cs @@ -421,5 +421,123 @@ public void GetEnumerableOfConcatCollectionChainFollowedByEnumerableNodeShouldBe Assert.Equal(0xf00, en.Current); } } + + [Theory] + [MemberData(nameof(GetToArrayDataSources))] + public void CollectionInterleavedWithLazyEnumerables_ToArray(IEnumerable[] arrays) + { + // See https://github.com/dotnet/corefx/issues/23680 + + IEnumerable concats = arrays[0]; + + for (int i = 1; i < arrays.Length; i++) + { + concats = concats.Concat(arrays[i]); + } + + int[] results = concats.ToArray(); + + for (int i = 0; i < results.Length; i++) + { + Assert.Equal(i, results[i]); + } + } + + private static IEnumerable GetToArrayDataSources() + { + // Marker at the end + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(new int[] { 0 }), + new TestEnumerable(new int[] { 1 }), + new TestEnumerable(new int[] { 2 }), + new int[] { 3 }, + } + }; + + // Marker at beginning + yield return new object[] + { + new IEnumerable[] + { + new int[] { 0 }, + new TestEnumerable(new int[] { 1 }), + new TestEnumerable(new int[] { 2 }), + new TestEnumerable(new int[] { 3 }), + } + }; + + // Marker in middle + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(new int[] { 0 }), + new int[] { 1 }, + new TestEnumerable(new int[] { 2 }), + } + }; + + // Non-marker in middle + yield return new object[] + { + new IEnumerable[] + { + new int[] { 0 }, + new TestEnumerable(new int[] { 1 }), + new int[] { 2 }, + } + }; + + // Big arrays (marker in middle) + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(Enumerable.Range(0, 100).ToArray()), + Enumerable.Range(100, 100).ToArray(), + new TestEnumerable(Enumerable.Range(200, 100).ToArray()), + } + }; + + // Big arrays (non-marker in middle) + yield return new object[] + { + new IEnumerable[] + { + Enumerable.Range(0, 100).ToArray(), + new TestEnumerable(Enumerable.Range(100, 100).ToArray()), + Enumerable.Range(200, 100).ToArray(), + } + }; + + // Interleaved (first marker) + yield return new object[] + { + new IEnumerable[] + { + new int[] { 0 }, + new TestEnumerable(new int[] { 1 }), + new int[] { 2 }, + new TestEnumerable(new int[] { 3 }), + new int[] { 4 }, + } + }; + + // Interleaved (first non-marker) + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(new int[] { 0 }), + new int[] { 1 }, + new TestEnumerable(new int[] { 2 }), + new int[] { 3 }, + new TestEnumerable(new int[] { 4 }), + } + }; + } } } diff --git a/src/System.Linq/tests/SelectManyTests.cs b/src/System.Linq/tests/SelectManyTests.cs index 10eb908b7249..1026e93698b5 100644 --- a/src/System.Linq/tests/SelectManyTests.cs +++ b/src/System.Linq/tests/SelectManyTests.cs @@ -477,5 +477,116 @@ public void ThrowOverflowExceptionOnConstituentLargeCounts(int[] counts) IEnumerable iterator = counts.SelectMany(c => Enumerable.Range(1, c)); Assert.Throws(() => iterator.Count()); } + + [Theory] + [MemberData(nameof(GetToArrayDataSources))] + public void CollectionInterleavedWithLazyEnumerables_ToArray(IEnumerable[] arrays) + { + // See https://github.com/dotnet/corefx/issues/23680 + + int[] results = arrays.SelectMany(ar => ar).ToArray(); + + for (int i = 0; i < results.Length; i++) + { + Assert.Equal(i, results[i]); + } + } + + private static IEnumerable GetToArrayDataSources() + { + // Marker at the end + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(new int[] { 0 }), + new TestEnumerable(new int[] { 1 }), + new TestEnumerable(new int[] { 2 }), + new int[] { 3 }, + } + }; + + // Marker at beginning + yield return new object[] + { + new IEnumerable[] + { + new int[] { 0 }, + new TestEnumerable(new int[] { 1 }), + new TestEnumerable(new int[] { 2 }), + new TestEnumerable(new int[] { 3 }), + } + }; + + // Marker in middle + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(new int[] { 0 }), + new int[] { 1 }, + new TestEnumerable(new int[] { 2 }), + } + }; + + // Non-marker in middle + yield return new object[] + { + new IEnumerable[] + { + new int[] { 0 }, + new TestEnumerable(new int[] { 1 }), + new int[] { 2 }, + } + }; + + // Big arrays (marker in middle) + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(Enumerable.Range(0, 100).ToArray()), + Enumerable.Range(100, 100).ToArray(), + new TestEnumerable(Enumerable.Range(200, 100).ToArray()), + } + }; + + // Big arrays (non-marker in middle) + yield return new object[] + { + new IEnumerable[] + { + Enumerable.Range(0, 100).ToArray(), + new TestEnumerable(Enumerable.Range(100, 100).ToArray()), + Enumerable.Range(200, 100).ToArray(), + } + }; + + // Interleaved (first marker) + yield return new object[] + { + new IEnumerable[] + { + new int[] { 0 }, + new TestEnumerable(new int[] { 1 }), + new int[] { 2 }, + new TestEnumerable(new int[] { 3 }), + new int[] { 4 }, + } + }; + + // Interleaved (first non-marker) + yield return new object[] + { + new IEnumerable[] + { + new TestEnumerable(new int[] { 0 }), + new int[] { 1 }, + new TestEnumerable(new int[] { 2 }), + new int[] { 3 }, + new TestEnumerable(new int[] { 4 }), + } + }; + } } }