From 29d529c814a4ff3dfa8c092760a7b7fba7d66f7f Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 12 Oct 2026 20:15:22 +1100 Subject: [PATCH] Block copy in List AddRange(ReadOnlySpan) and CopyTo(Span) AddRange added one element at a time without reserving capacity, and CopyTo copied through the indexer. Reserve capacity up front, and block copy larger spans. --- src/Polyfill/Polyfill_List.cs | 89 +++++++++++- src/Split/net461/Polyfill_List.cs | 66 ++++++++- src/Split/net462/Polyfill_List.cs | 66 ++++++++- src/Split/net47/Polyfill_List.cs | 66 ++++++++- src/Split/net471/Polyfill_List.cs | 66 ++++++++- src/Split/net472/Polyfill_List.cs | 66 ++++++++- src/Split/net48/Polyfill_List.cs | 66 ++++++++- src/Split/net481/Polyfill_List.cs | 66 ++++++++- src/Split/net5.0/Polyfill_List.cs | 50 ++++++- src/Split/net6.0/Polyfill_List.cs | 50 ++++++- src/Split/net7.0/Polyfill_List.cs | 50 ++++++- src/Split/netcoreapp2.0/Polyfill_List.cs | 65 ++++++++- src/Split/netcoreapp2.1/Polyfill_List.cs | 65 ++++++++- src/Split/netcoreapp2.2/Polyfill_List.cs | 65 ++++++++- src/Split/netcoreapp3.0/Polyfill_List.cs | 65 ++++++++- src/Split/netcoreapp3.1/Polyfill_List.cs | 65 ++++++++- src/Split/netstandard2.0/Polyfill_List.cs | 66 ++++++++- src/Split/netstandard2.1/Polyfill_List.cs | 66 ++++++++- src/Split/uap10.0/Polyfill_List.cs | 66 ++++++++- src/Tests/PolyfillTests_List.cs | 163 ++++++++++++++++++++++ 20 files changed, 1343 insertions(+), 44 deletions(-) diff --git a/src/Polyfill/Polyfill_List.cs b/src/Polyfill/Polyfill_List.cs index 78bf2b0f..3f666e96 100644 --- a/src/Polyfill/Polyfill_List.cs +++ b/src/Polyfill/Polyfill_List.cs @@ -20,12 +20,75 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.addrange?view=net-11.0 public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + + var length = source.Length; + + // List.AddRange block copies an ICollection. On .NET Framework it goes through an extra + // temporary array, which only pays off when the items can be block copied without GC bookkeeping. + if (length >= blockCopyThreshold +#if !NETCOREAPP + && typeof(T).IsPrimitive +#endif + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + + return; + } + + // Grow once, using the same doubling as List, instead of several times while adding + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + + if (newCapacity < required) + { + newCapacity = required; + } + + target.Capacity = newCapacity; + } + foreach (var item in source) { target.Add(item); } } + // Below this the cost of renting a buffer outweighs the block copy + const int blockCopyThreshold = 64; + + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + // Do not keep references alive in the pool + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + + pool.Return(buffer); + } + /// Inserts the elements of a span into the at the specified index. //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.insertrange?view=net-11.0 public static void InsertRange(this List target, int index, ReadOnlySpan source) @@ -51,10 +114,32 @@ public static void CopyTo(this List target, Span destination) throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) +#if NET5_0_OR_GREATER + System.Runtime.InteropServices.CollectionsMarshal.AsSpan(target).CopyTo(destination); +#else + var count = target.Count; + if (count < blockCopyThreshold) { - destination[index] = target[index]; + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + + return; + } + + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); } + finally + { + ReturnToPool(pool, buffer, count); + } +#endif } #endif diff --git a/src/Split/net461/Polyfill_List.cs b/src/Split/net461/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net461/Polyfill_List.cs +++ b/src/Split/net461/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net462/Polyfill_List.cs b/src/Split/net462/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net462/Polyfill_List.cs +++ b/src/Split/net462/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net47/Polyfill_List.cs b/src/Split/net47/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net47/Polyfill_List.cs +++ b/src/Split/net47/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net471/Polyfill_List.cs b/src/Split/net471/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net471/Polyfill_List.cs +++ b/src/Split/net471/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net472/Polyfill_List.cs b/src/Split/net472/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net472/Polyfill_List.cs +++ b/src/Split/net472/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net48/Polyfill_List.cs b/src/Split/net48/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net48/Polyfill_List.cs +++ b/src/Split/net48/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net481/Polyfill_List.cs b/src/Split/net481/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/net481/Polyfill_List.cs +++ b/src/Split/net481/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/net5.0/Polyfill_List.cs b/src/Split/net5.0/Polyfill_List.cs index 7a9348d0..867c077f 100644 --- a/src/Split/net5.0/Polyfill_List.cs +++ b/src/Split/net5.0/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,10 +83,7 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) - { - destination[index] = target[index]; - } + System.Runtime.InteropServices.CollectionsMarshal.AsSpan(target).CopyTo(destination); } #endif /// diff --git a/src/Split/net6.0/Polyfill_List.cs b/src/Split/net6.0/Polyfill_List.cs index 266ba293..cd3f4899 100644 --- a/src/Split/net6.0/Polyfill_List.cs +++ b/src/Split/net6.0/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,10 +83,7 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) - { - destination[index] = target[index]; - } + System.Runtime.InteropServices.CollectionsMarshal.AsSpan(target).CopyTo(destination); } #endif } diff --git a/src/Split/net7.0/Polyfill_List.cs b/src/Split/net7.0/Polyfill_List.cs index 22d8120e..61aeef7a 100644 --- a/src/Split/net7.0/Polyfill_List.cs +++ b/src/Split/net7.0/Polyfill_List.cs @@ -10,11 +10,56 @@ static partial class Polyfill /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -35,10 +80,7 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) - { - destination[index] = target[index]; - } + System.Runtime.InteropServices.CollectionsMarshal.AsSpan(target).CopyTo(destination); } #endif } diff --git a/src/Split/netcoreapp2.0/Polyfill_List.cs b/src/Split/netcoreapp2.0/Polyfill_List.cs index 7a9348d0..3b977cba 100644 --- a/src/Split/netcoreapp2.0/Polyfill_List.cs +++ b/src/Split/netcoreapp2.0/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +83,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/netcoreapp2.1/Polyfill_List.cs b/src/Split/netcoreapp2.1/Polyfill_List.cs index 7a9348d0..3b977cba 100644 --- a/src/Split/netcoreapp2.1/Polyfill_List.cs +++ b/src/Split/netcoreapp2.1/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +83,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/netcoreapp2.2/Polyfill_List.cs b/src/Split/netcoreapp2.2/Polyfill_List.cs index 7a9348d0..3b977cba 100644 --- a/src/Split/netcoreapp2.2/Polyfill_List.cs +++ b/src/Split/netcoreapp2.2/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +83,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/netcoreapp3.0/Polyfill_List.cs b/src/Split/netcoreapp3.0/Polyfill_List.cs index 7a9348d0..3b977cba 100644 --- a/src/Split/netcoreapp3.0/Polyfill_List.cs +++ b/src/Split/netcoreapp3.0/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +83,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/netcoreapp3.1/Polyfill_List.cs b/src/Split/netcoreapp3.1/Polyfill_List.cs index 7a9348d0..3b977cba 100644 --- a/src/Split/netcoreapp3.1/Polyfill_List.cs +++ b/src/Split/netcoreapp3.1/Polyfill_List.cs @@ -13,11 +13,56 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +83,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/netstandard2.0/Polyfill_List.cs b/src/Split/netstandard2.0/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/netstandard2.0/Polyfill_List.cs +++ b/src/Split/netstandard2.0/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/netstandard2.1/Polyfill_List.cs b/src/Split/netstandard2.1/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/netstandard2.1/Polyfill_List.cs +++ b/src/Split/netstandard2.1/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Split/uap10.0/Polyfill_List.cs b/src/Split/uap10.0/Polyfill_List.cs index 7a9348d0..ff7d7a5a 100644 --- a/src/Split/uap10.0/Polyfill_List.cs +++ b/src/Split/uap10.0/Polyfill_List.cs @@ -13,11 +13,57 @@ public static ReadOnlyCollection AsReadOnly(this IList target) => /// Adds the elements of the specified span to the end of the . public static void AddRange(this List target, ReadOnlySpan source) { + if (source.IsEmpty) + { + return; + } + var length = source.Length; + if (length >= blockCopyThreshold + && typeof(T).IsPrimitive + ) + { + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(length); + try + { + source.CopyTo(buffer); + target.AddRange(new ArraySegment(buffer, 0, length)); + } + finally + { + ReturnToPool(pool, buffer, length); + } + return; + } + var required = target.Count + length; + var capacity = target.Capacity; + if (required > capacity) + { + var newCapacity = capacity * 2; + if ((uint) newCapacity > 0x7FEFFFFF) + { + newCapacity = 0x7FEFFFFF; + } + if (newCapacity < required) + { + newCapacity = required; + } + target.Capacity = newCapacity; + } foreach (var item in source) { target.Add(item); } } + const int blockCopyThreshold = 64; + static void ReturnToPool(System.Buffers.ArrayPool pool, T[] buffer, int length) + { + if (!typeof(T).IsPrimitive) + { + Array.Clear(buffer, 0, length); + } + pool.Return(buffer); + } /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { @@ -38,9 +84,25 @@ public static void CopyTo(this List target, Span destination) { throw new ArgumentException("Destination is too short.", nameof(destination)); } - for (var index = 0; index < target.Count; index++) + var count = target.Count; + if (count < blockCopyThreshold) + { + for (var index = 0; index < count; index++) + { + destination[index] = target[index]; + } + return; + } + var pool = System.Buffers.ArrayPool.Shared; + var buffer = pool.Rent(count); + try + { + target.CopyTo(0, buffer, 0, count); + new ReadOnlySpan(buffer, 0, count).CopyTo(destination); + } + finally { - destination[index] = target[index]; + ReturnToPool(pool, buffer, count); } } #endif diff --git a/src/Tests/PolyfillTests_List.cs b/src/Tests/PolyfillTests_List.cs index 29beb667..d51f6d5d 100644 --- a/src/Tests/PolyfillTests_List.cs +++ b/src/Tests/PolyfillTests_List.cs @@ -79,4 +79,167 @@ public async Task List_TrimExcess() // Should not throw await Assert.That(list.Count).IsEqualTo(3); } + [Test] + [Arguments(0, 0)] + [Arguments(0, 1)] + [Arguments(0, 63)] + [Arguments(0, 64)] + [Arguments(0, 65)] + [Arguments(3, 0)] + [Arguments(3, 1)] + [Arguments(3, 2)] + [Arguments(3, 64)] + [Arguments(100, 5)] + [Arguments(100, 5000)] + public async Task List_AddRange_Span_MatchesAddRangeOfArray(int initialCount, int addCount) + { + var added = Enumerable.Range(1000, addCount).ToArray(); + var expected = Enumerable.Range(0, initialCount).ToList(); + expected.AddRange((IEnumerable) added); + + var list = Enumerable.Range(0, initialCount).ToList(); + list.AddRange(added.AsSpan()); + await Assert.That(list.Count).IsEqualTo(initialCount + addCount); + await Assert.That(list.SequenceEqual(expected)).IsTrue(); + + // same again for a reference type + var addedStrings = added.Select(_ => _ % 7 == 0 ? null : _.ToString()).ToArray(); + var expectedStrings = Enumerable.Range(0, initialCount).Select(_ => (string?) _.ToString()).ToList(); + expectedStrings.AddRange((IEnumerable) addedStrings); + + var strings = Enumerable.Range(0, initialCount).Select(_ => (string?) _.ToString()).ToList(); + strings.AddRange(addedStrings.AsSpan()); + await Assert.That(strings.SequenceEqual(expectedStrings)).IsTrue(); + + // and for a struct that is not a primitive + var addedPairs = added.Select(_ => new KeyValuePair(_, _.ToString())).ToArray(); + var pairs = new List>(); + pairs.AddRange(addedPairs.AsSpan()); + await Assert.That(pairs.SequenceEqual(addedPairs)).IsTrue(); + } + + [Test] + public async Task List_AddRange_Span_Repeated() + { + var list = new List(); + var expected = new List(); + for (var round = 0; round < 200; round++) + { + var chunk = Enumerable.Range(round * 1000, round % 9 == 0 ? 150 : round % 5).ToArray(); + list.AddRange(chunk.AsSpan()); + expected.AddRange((IEnumerable) chunk); + } + + await Assert.That(list.SequenceEqual(expected)).IsTrue(); + } + + [Test] + public async Task List_AddRange_Span_GrowthIsAmortized() + { + // appending many small spans must not reallocate for every call + var list = new List(); + var reallocations = 0; + var capacity = list.Capacity; + int[] chunk = [1, 2, 3]; + for (var round = 0; round < 1000; round++) + { + list.AddRange(chunk.AsSpan()); + if (list.Capacity != capacity) + { + capacity = list.Capacity; + reallocations++; + } + } + + await Assert.That(list.Count).IsEqualTo(3000); + await Assert.That(reallocations).IsLessThan(20); + } + + [Test] + [Arguments(2)] + [Arguments(200)] + public async Task List_AddRange_Span_OfItself(int count) + { + var list = Enumerable.Range(0, count).ToList(); + list.AddRange(CollectionsMarshal.AsSpan(list)); + await Assert.That(list.SequenceEqual(Enumerable.Range(0, count).Concat(Enumerable.Range(0, count)))).IsTrue(); + } + + [Test] + public async Task List_AddRange_Span_Empty_DoesNotInvalidateEnumerators() + { + var list = new List {1, 2, 3}; + var seen = 0; + foreach (var _ in list) + { + list.AddRange(ReadOnlySpan.Empty); + seen++; + } + + await Assert.That(seen).IsEqualTo(3); + } + + [Test] + [Arguments(1)] + [Arguments(100)] + public async Task List_AddRange_Span_NonEmpty_InvalidatesEnumerators(int count) + { + var list = new List {1, 2, 3}; + var added = new int[count]; + using IEnumerator enumerator = list.GetEnumerator(); + enumerator.MoveNext(); + list.AddRange(added.AsSpan()); + var threw = false; + try + { + enumerator.MoveNext(); + } + catch (InvalidOperationException) + { + threw = true; + } + + await Assert.That(threw).IsTrue(); + } + + [Test] + [Arguments(0)] + [Arguments(1)] + [Arguments(63)] + [Arguments(64)] + [Arguments(65)] + [Arguments(5000)] + public async Task List_CopyTo_Span_CopiesAllItems(int count) + { + var list = Enumerable.Range(10, count).ToList(); + // spare capacity must not be copied + list.Capacity = count + 10; + + var exact = new int[count]; + list.CopyTo(exact.AsSpan()); + await Assert.That(exact.SequenceEqual(list)).IsTrue(); + + // a longer destination keeps its tail, and an offset destination its head + var longer = Enumerable.Repeat(-1, count + 4).ToArray(); + list.CopyTo(longer.AsSpan(2)); + await Assert.That(longer.Take(2).All(_ => _ == -1)).IsTrue(); + await Assert.That(longer.Skip(2).Take(count).SequenceEqual(list)).IsTrue(); + await Assert.That(longer.Skip(2 + count).All(_ => _ == -1)).IsTrue(); + + var strings = list.Select(_ => _ % 7 == 0 ? null : _.ToString()).ToList(); + var stringDestination = new string?[count]; + strings.CopyTo(stringDestination.AsSpan()); + await Assert.That(((IEnumerable) stringDestination).SequenceEqual(strings)).IsTrue(); + // the list is left as it was + await Assert.That(list.SequenceEqual(Enumerable.Range(10, count))).IsTrue(); + } + + [Test] + public async Task List_CopyTo_Span_TooShort_Large_Throws() + { + var list = Enumerable.Range(0, 200).ToList(); + var destination = new int[199]; + await Assert.That(() => list.CopyTo(destination.AsSpan())).Throws(); + await Assert.That(destination.All(_ => _ == 0)).IsTrue(); + } }