From 26a33a30d296ddf962047a5497e67ca9536c6ae3 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Sat, 5 Jul 2025 14:21:12 +0200 Subject: [PATCH] Fixed nullability warnings in virtualized collections --- Nuclex.Avalonia (netstandard-2.0).csproj | 6 +- .../AsyncVirtualObservableReadOnlyList.cs | 66 ++++++++++++------- .../VirtualObservableReadOnlyList.cs | 55 ++++++++++------ 3 files changed, 81 insertions(+), 46 deletions(-) diff --git a/Nuclex.Avalonia (netstandard-2.0).csproj b/Nuclex.Avalonia (netstandard-2.0).csproj index 54ab9d9..fdae37b 100644 --- a/Nuclex.Avalonia (netstandard-2.0).csproj +++ b/Nuclex.Avalonia (netstandard-2.0).csproj @@ -9,7 +9,7 @@ Nuclex.Avalonia obj\source enable - 8.0 + 9.0 @@ -25,6 +25,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/Collections/AsyncVirtualObservableReadOnlyList.cs b/Source/Collections/AsyncVirtualObservableReadOnlyList.cs index 1171f1b..7cb43bd 100644 --- a/Source/Collections/AsyncVirtualObservableReadOnlyList.cs +++ b/Source/Collections/AsyncVirtualObservableReadOnlyList.cs @@ -20,12 +20,16 @@ limitations under the License. using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; #if !NO_SPECIALIZED_COLLECTIONS using System.Collections.Specialized; -using System.Diagnostics; #endif +using Nuclex.Support.Collections; + namespace Nuclex.Avalonia.Collections { /// @@ -59,7 +63,7 @@ namespace Nuclex.Avalonia.Collections { /// Immediately releases all resources owned by the instance public void Dispose() { - this.virtualList = null; + this.virtualList = null!; // Only to make life easier got the GC } /// The item at the enumerator's current position @@ -119,7 +123,7 @@ namespace Nuclex.Avalonia.Collections { /// The item at the enumerator's current position object IEnumerator.Current { - get { return Current; } + get { return Current!; } // No idea what the compiler's issue is here } #if DEBUG @@ -145,24 +149,24 @@ namespace Nuclex.Avalonia.Collections { #endregion // class Enumerator /// Raised when an item has been added to the collection - public event EventHandler> ItemAdded; + public event EventHandler>? ItemAdded; /// Raised when an item is removed from the collection - public event EventHandler> ItemRemoved; + public event EventHandler>? ItemRemoved; /// Raised when an item is replaced in the collection - public event EventHandler> ItemReplaced; + public event EventHandler>? ItemReplaced; /// Raised when the collection is about to be cleared /// /// This could be covered by calling ItemRemoved for each item currently /// contained in the collection, but it is often simpler and more efficient /// to process the clearing of the entire collection as a special operation. /// - public event EventHandler Clearing; + public event EventHandler? Clearing; /// Raised when the collection has been cleared - public event EventHandler Cleared; + public event EventHandler? Cleared; #if !NO_SPECIALIZED_COLLECTIONS /// Called when the collection has changed - public event NotifyCollectionChangedEventHandler CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged; #endif /// @@ -178,9 +182,10 @@ namespace Nuclex.Avalonia.Collections { /// performance from requesting multiple items at once. /// public AsyncVirtualObservableReadOnlyList(int pageSize = 32) { - this.typedList = new List(); - this.objectList = this.typedList as IList; + this.typedList = new TItem[0]; + this.objectList = (IList)this.typedList; this.pageSize = pageSize; + this.fetchedPages = new bool[0]; } /// @@ -201,7 +206,7 @@ namespace Nuclex.Avalonia.Collections { if(purgeItems) { int itemCount = this.assumedCount.Value; for(int index = 0; index < itemCount; ++index) { - this.typedList[index] = default(TItem); + this.typedList[index] = default(TItem)!; // not going to be exposed to users } } } @@ -234,7 +239,7 @@ namespace Nuclex.Avalonia.Collections { this.pageSize ); for(int index = itemIndex / this.pageSize; index < count; ++index) { - this.typedList[index] = default(TItem); + this.typedList[index] = default(TItem)!; // not going to be exposed to users } } } @@ -244,7 +249,19 @@ namespace Nuclex.Avalonia.Collections { /// Item whose index will be determined /// The index of the item in the list or -1 if not found public int IndexOf(TItem item) { - return this.typedList.IndexOf(item); + requireCount(); + requireAllPages(); + + // TODO: this won't work, it will compare the placeholder items :-/ + + IComparer itemComparer = Comparer.Default; + for(int index = 0; index < this.assumedCount.Value; ++index) { + if(itemComparer.Compare(this.typedList[index], item) == 0) { + return index; + } + } + + return -1; } /// Inserts an item into the list at the specified index @@ -299,10 +316,7 @@ namespace Nuclex.Avalonia.Collections { /// Item the list will be checked for /// True if the list contains the specified items public bool Contains(TItem item) { - requireCount(); - requireAllPages(); - - return this.typedList.Contains(item); + return (IndexOf(item) != -1); } /// Copies the contents of the list into an array @@ -570,6 +584,7 @@ namespace Nuclex.Avalonia.Collections { ); /// Ensures that the total number of items is known + [MemberNotNull(nameof(assumedCount))] private void requireCount() { if(!this.assumedCount.HasValue) { int itemCount = CountItems(); @@ -589,11 +604,12 @@ namespace Nuclex.Avalonia.Collections { this.assumedCount.HasValue, "This method should only be called when item count is already known" ); - int pageCount = this.fetchedPages.Length; + + // We may find that the list is shorter than expected while requesting pages. + // But the results will come in asynchronously, so we can't wait for it. + int pageCount = (this.assumedCount!.Value + this.pageSize - 1) / this.pageSize; for(int index = 0; index < pageCount; ++index) { requirePage(index); - // CHECK: Should we throttle this by constructing a clever chain of - // ContinueWith() tasks so we don't cause a flood of async requests? } } @@ -609,7 +625,7 @@ namespace Nuclex.Avalonia.Collections { } int startIndex = pageIndex * this.pageSize; - int count = Math.Min(this.assumedCount.Value - startIndex, this.pageSize); + int count = Math.Min(this.assumedCount!.Value - startIndex, this.pageSize); CreatePlaceholderItems(this.typedList, pageIndex * this.pageSize, count); this.fetchedPages[pageIndex] = true; // Prevent double-fetch @@ -619,7 +635,7 @@ namespace Nuclex.Avalonia.Collections { var placeholderItems = new List(count); for(int index = startIndex; index < count; ++index) { placeholderItems[index - startIndex] = this.typedList[index]; - OnReplaced(default(TItem), this.typedList[index], index); + //OnReplaced(default(TItem), this.typedList[index], index); } int fetchedItemCount = await FetchItemsAsync(this.typedList, startIndex, count); @@ -630,7 +646,7 @@ namespace Nuclex.Avalonia.Collections { // The count may have been adjusted if this truncated the list, // so recalculate the actual number of items. Then send out change // notifications for the items that have now been fetched. - count = Math.Min(this.assumedCount.Value - startIndex, this.pageSize); + count = Math.Min(this.assumedCount!.Value - startIndex, this.pageSize); for(int index = startIndex; index < count; ++index) { OnReplaced(placeholderItems[index - startIndex], this.typedList[index], index); } @@ -643,7 +659,7 @@ namespace Nuclex.Avalonia.Collections { /// Tracks which pages have been fetched so far private bool[] fetchedPages; /// The wrapped list under its type-safe interface - private IList typedList; + private TItem[] typedList; /// The wrapped list under its object interface private IList objectList; #if DEBUG diff --git a/Source/Collections/VirtualObservableReadOnlyList.cs b/Source/Collections/VirtualObservableReadOnlyList.cs index dfc925f..3f7e668 100644 --- a/Source/Collections/VirtualObservableReadOnlyList.cs +++ b/Source/Collections/VirtualObservableReadOnlyList.cs @@ -20,12 +20,16 @@ limitations under the License. using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + #if !NO_SPECIALIZED_COLLECTIONS using System.Collections.Specialized; -using System.Diagnostics; #endif +using Nuclex.Support.Collections; + namespace Nuclex.Avalonia.Collections { /// @@ -59,7 +63,7 @@ namespace Nuclex.Avalonia.Collections { /// Immediately releases all resources owned by the instance public void Dispose() { - this.virtualList = null; + this.virtualList = null!; // Only to make life easier got the GC } /// The item at the enumerator's current position @@ -119,7 +123,7 @@ namespace Nuclex.Avalonia.Collections { /// The item at the enumerator's current position object IEnumerator.Current { - get { return Current; } + get { return Current!; } // No idea what the compiler's issue is here } #if DEBUG @@ -145,24 +149,24 @@ namespace Nuclex.Avalonia.Collections { #endregion // class Enumerator /// Raised when an item has been added to the collection - public event EventHandler> ItemAdded; + public event EventHandler>? ItemAdded; /// Raised when an item is removed from the collection - public event EventHandler> ItemRemoved; + public event EventHandler>? ItemRemoved; /// Raised when an item is replaced in the collection - public event EventHandler> ItemReplaced; + public event EventHandler>? ItemReplaced; /// Raised when the collection is about to be cleared /// /// This could be covered by calling ItemRemoved for each item currently /// contained in the collection, but it is often simpler and more efficient /// to process the clearing of the entire collection as a special operation. /// - public event EventHandler Clearing; + public event EventHandler? Clearing; /// Raised when the collection has been cleared - public event EventHandler Cleared; + public event EventHandler? Cleared; #if !NO_SPECIALIZED_COLLECTIONS /// Called when the collection has changed - public event NotifyCollectionChangedEventHandler CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged; #endif /// @@ -178,9 +182,10 @@ namespace Nuclex.Avalonia.Collections { /// performance from requesting multiple items at once. /// public VirtualObservableReadOnlyList(int pageSize = 32) { - this.typedList = new List(); - this.objectList = this.typedList as IList; + this.typedList = new TItem[0]; + this.objectList = (IList)this.typedList; this.pageSize = pageSize; + this.fetchedPages = new bool[0]; } /// @@ -201,7 +206,7 @@ namespace Nuclex.Avalonia.Collections { if(purgeItems) { int itemCount = this.assumedCount.Value; for(int index = 0; index < itemCount; ++index) { - this.typedList[index] = default(TItem); + this.typedList[index] = default(TItem)!; // not going to be exposed to users } } } @@ -234,7 +239,7 @@ namespace Nuclex.Avalonia.Collections { this.pageSize ); for(int index = itemIndex / this.pageSize; index < count; ++index) { - this.typedList[index] = default(TItem); + this.typedList[index] = default(TItem)!; // not going to be exposed to users } } } @@ -244,7 +249,19 @@ namespace Nuclex.Avalonia.Collections { /// Item whose index will be determined /// The index of the item in the list or -1 if not found public int IndexOf(TItem item) { - return this.typedList.IndexOf(item); + requireCount(); + requireAllPages(); + + // TODO: this won't work, it will compare the placeholder items :-/ + + IComparer itemComparer = Comparer.Default; + for(int index = 0; index < this.assumedCount.Value; ++index) { + if(itemComparer.Compare(this.typedList[index], item) == 0) { + return index; + } + } + + return -1; } /// Inserts an item into the list at the specified index @@ -299,10 +316,7 @@ namespace Nuclex.Avalonia.Collections { /// Item the list will be checked for /// True if the list contains the specified items public bool Contains(TItem item) { - requireCount(); - requireAllPages(); - - return this.typedList.Contains(item); + return (IndexOf(item) != -1); } /// Copies the contents of the list into an array @@ -342,7 +356,7 @@ namespace Nuclex.Avalonia.Collections { /// Returns an enumerator for the items in the list /// An enumerator for the list's items public IEnumerator GetEnumerator() { - return this.typedList.GetEnumerator(); // TODO + return new Enumerator(this); } #region IEnumerable implementation @@ -550,6 +564,7 @@ namespace Nuclex.Avalonia.Collections { protected abstract int FetchItems(IList target, int startIndex, int count); /// Ensures that the total number of items is known + [MemberNotNull(nameof(assumedCount))] private void requireCount() { if(!this.assumedCount.HasValue) { int itemCount = CountItems(); @@ -604,7 +619,7 @@ namespace Nuclex.Avalonia.Collections { /// Tracks which pages have been fetched so far private bool[] fetchedPages; /// The wrapped list under its type-safe interface - private IList typedList; + private TItem[] typedList; /// The wrapped list under its object interface private IList objectList; #if DEBUG