From 5fd827cce876537c04ddbce63f255438203349d8 Mon Sep 17 00:00:00 2001 From: prozolic <42107886+prozolic@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:54:09 +0900 Subject: [PATCH] Fix List to ListView casting bug on .NET Framework --- .../Shims/Collections.cs | 121 ++++++++++++------ .../Shims/CollectionsMarshalEx.cs | 50 +++++++- 2 files changed, 122 insertions(+), 49 deletions(-) diff --git a/src/ObservableCollections/Shims/Collections.cs b/src/ObservableCollections/Shims/Collections.cs index c4ae823..7add725 100644 --- a/src/ObservableCollections/Shims/Collections.cs +++ b/src/ObservableCollections/Shims/Collections.cs @@ -45,16 +45,32 @@ public static void AddRange(this List list, ReadOnlySpan source) { if (!source.IsEmpty) { - ref var view = ref Unsafe.As, CollectionsMarshal.ListView>(ref list!); - - if (view._items.Length - view._size < source.Length) - { - Grow(ref view, checked(view._size + source.Length)); + if (!CollectionsMarshal.IsLegacyList) + { + ref var view = ref Unsafe.As, CollectionsMarshal.ListView>(ref list!); + + if (view._items.Length - view._size < source.Length) + { + Grow(ref view._items, view._size, checked(view._size + source.Length)); + } + + source.CopyTo(view._items.AsSpan(view._size)); + view._size += source.Length; + view._version++; + } + else + { + ref var view = ref Unsafe.As, CollectionsMarshal.LegacyListView>(ref list!); + + if (view._items.Length - view._size < source.Length) + { + Grow(ref view._items, view._size, checked(view._size + source.Length)); + } + + source.CopyTo(view._items.AsSpan(view._size)); + view._size += source.Length; + view._version++; } - - source.CopyTo(view._items.AsSpan(view._size)); - view._size += source.Length; - view._version++; } } @@ -62,53 +78,74 @@ public static void AddRange(this List list, ReadOnlySpan source) public static void InsertRange(this List list, int index, ReadOnlySpan source) { if (!source.IsEmpty) - { - ref var view = ref Unsafe.As, CollectionsMarshal.ListView>(ref list!); - - if (view._items.Length - view._size < source.Length) - { - Grow(ref view, checked(view._size + source.Length)); + { + if (!CollectionsMarshal.IsLegacyList) + { + ref var view = ref Unsafe.As, CollectionsMarshal.ListView>(ref list!); + + if (view._items.Length - view._size < source.Length) + { + Grow(ref view._items, view._size, checked(view._size + source.Length)); + } + + if (index < view._size) + { + Array.Copy(view._items, index, view._items, index + source.Length, view._size - index); + } + + source.CopyTo(view._items.AsSpan(index)); + view._size += source.Length; + view._version++; + } + else + { + ref var view = ref Unsafe.As, CollectionsMarshal.LegacyListView>(ref list!); + + if (view._items.Length - view._size < source.Length) + { + Grow(ref view._items, view._size, checked(view._size + source.Length)); + } + + if (index < view._size) + { + Array.Copy(view._items, index, view._items, index + source.Length, view._size - index); + } + + source.CopyTo(view._items.AsSpan(index)); + view._size += source.Length; + view._version++; } - - if (index < view._size) - { - Array.Copy(view._items, index, view._items, index + source.Length, view._size - index); - } - - source.CopyTo(view._items.AsSpan(index)); - view._size += source.Length; - view._version++; } - } - - static void Grow(ref CollectionsMarshal.ListView list, int capacity) + } + + static void Grow(ref T[] items, int size, int capacity) { - SetCapacity(ref list, GetNewCapacity(ref list, capacity)); - } - - static void SetCapacity(ref CollectionsMarshal.ListView list, int value) + SetCapacity(ref items, size, GetNewCapacity(items.Length, capacity)); + } + + static void SetCapacity(ref T[] items, int size, int capacity) { - if (value != list._items.Length) + if (capacity != items.Length) { - if (value > 0) + if (capacity > 0) { - T[] newItems = new T[value]; - if (list._size > 0) + T[] newItems = new T[capacity]; + if (size > 0) { - Array.Copy(list._items, newItems, list._size); + Array.Copy(items, newItems, size); } - list._items = newItems; + items = newItems; } else { - list._items = Array.Empty(); + items = Array.Empty(); } } - } - - static int GetNewCapacity(ref CollectionsMarshal.ListView list, int capacity) + } + + static int GetNewCapacity(int length, int capacity) { - int newCapacity = list._items.Length == 0 ? 4 : 2 * list._items.Length; + int newCapacity = length == 0 ? 4 : 2 * length; if ((uint)newCapacity > ArrayMaxLength) newCapacity = ArrayMaxLength; diff --git a/src/ObservableCollections/Shims/CollectionsMarshalEx.cs b/src/ObservableCollections/Shims/CollectionsMarshalEx.cs index 6541623..f58fb71 100644 --- a/src/ObservableCollections/Shims/CollectionsMarshalEx.cs +++ b/src/ObservableCollections/Shims/CollectionsMarshalEx.cs @@ -12,23 +12,59 @@ namespace System.Runtime.InteropServices; internal static class CollectionsMarshal { + internal static readonly bool IsLegacyList; + +#if NETSTANDARD2_0 || NETSTANDARD2_1 + static CollectionsMarshal() + { + int listSize = 0; + try + { + listSize = typeof(List<>).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Length; + } + catch + { + listSize = 3; + } + + // In .NET Framework, List has a _syncRoot field, so the number of fields becomes 4. + IsLegacyList = listSize == 4; + } +#endif + /// /// similar as AsSpan but modify size to create fixed-size span. /// public static Span AsSpan(List? list) { - if (list is null) return default; - - ref var view = ref Unsafe.As, ListView>(ref list!); - return view._items.AsSpan(0, view._size); - } - + if (list is null) return default; + + if (IsLegacyList) + { + ref var view = ref Unsafe.As, LegacyListView>(ref list!); + return view._items.AsSpan(0, list.Count); + } + else + { + ref var view = ref Unsafe.As, ListView>(ref list!); + return view._items.AsSpan(0, list.Count); + } + } + internal sealed class ListView { public T[] _items; public int _size; public int _version; + } + + internal sealed class LegacyListView + { + public T[] _items; + public int _size; + public int _version; + public Object _syncRoot; // in .NET Framework } } -#endif \ No newline at end of file +#endif