You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by ni...@apache.org on 2023/04/11 16:34:33 UTC

[lucenenet] branch master updated: BREAKING: Lucene.Net.Util.PriorityQueue: Refactored to remove the GetSentinelObject() virtual method that is called inside of the constructor and replaced it with ISentinelFactory. This also removes ambiguity since both "prepopulate" had to be specified as "true" in the constructor AND the GetSentinelObject() method had to be overridden for it to work. Now there is a single signal. Pass null to not populate. Pass a ISentinelFactory implementation to populate.

This is an automated email from the ASF dual-hosted git repository.

nightowl888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git


The following commit(s) were added to refs/heads/master by this push:
     new 2111f95e2 BREAKING: Lucene.Net.Util.PriorityQueue<T>: Refactored to remove the GetSentinelObject() virtual method that is called inside of the constructor and replaced it with ISentinelFactory<T>. This also removes ambiguity since both "prepopulate" had to be specified as "true" in the constructor AND the GetSentinelObject() method had to be overridden for it to work. Now there is a single signal. Pass null to not populate. Pass a ISentinelFactory<T> implementation to populate.
2111f95e2 is described below

commit 2111f95e27b498a56626c31f76badfd9c82532be
Author: Shad Storhaug <sh...@shadstorhaug.com>
AuthorDate: Tue Apr 11 21:10:59 2023 +0700

    BREAKING: Lucene.Net.Util.PriorityQueue<T>: Refactored to remove the GetSentinelObject() virtual method that is called inside of the constructor and replaced it with ISentinelFactory<T>. This also removes ambiguity since both "prepopulate" had to be specified as "true" in the constructor AND the GetSentinelObject() method had to be overridden for it to work. Now there is a single signal. Pass null to not populate. Pass a ISentinelFactory<T> implementation to populate.
---
 src/Lucene.Net.Benchmark/ByTask/Tasks/ReadTask.cs  |   2 +-
 .../Quality/Utils/QualityQueriesFinder.cs          |   7 +-
 src/Lucene.Net.Facet/TopOrdAndFloatQueue.cs        |  15 +-
 src/Lucene.Net.Facet/TopOrdAndIntQueue.cs          |  16 +-
 src/Lucene.Net.Misc/Misc/HighFreqTerms.cs          |   6 +-
 .../Queries/FuzzyLikeThisQuery.cs                  |   4 +-
 src/Lucene.Net.Suggest/Suggest/Lookup.cs           |  11 +-
 src/Lucene.Net.Tests/Util/TestPriorityQueue.cs     |  28 ++-
 src/Lucene.Net/Index/MultiTermsEnum.cs             |  10 +-
 src/Lucene.Net/Search/FieldValueHitQueue.cs        |  36 +++-
 src/Lucene.Net/Search/HitQueue.cs                  |  39 +++-
 src/Lucene.Net/Search/IndexSearcher.cs             |   4 +-
 src/Lucene.Net/Search/Spans/SpanOrQuery.cs         |   2 +-
 src/Lucene.Net/Search/TopDocs.cs                   |  42 +++-
 src/Lucene.Net/Search/TopFieldCollector.cs         |  14 +-
 src/Lucene.Net/Search/TopScoreDocCollector.cs      |  51 +++--
 src/Lucene.Net/Util/Fst/FST.cs                     |  11 +-
 src/Lucene.Net/Util/OfflineSorter.cs               |   6 +
 src/Lucene.Net/Util/PriorityQueue.cs               | 224 ++++++++++++++-------
 src/Lucene.Net/Util/WAH8DocIdSet.cs                |  17 +-
 20 files changed, 397 insertions(+), 148 deletions(-)

diff --git a/src/Lucene.Net.Benchmark/ByTask/Tasks/ReadTask.cs b/src/Lucene.Net.Benchmark/ByTask/Tasks/ReadTask.cs
index d9f32b1f0..21540eb9c 100644
--- a/src/Lucene.Net.Benchmark/ByTask/Tasks/ReadTask.cs
+++ b/src/Lucene.Net.Benchmark/ByTask/Tasks/ReadTask.cs
@@ -205,7 +205,7 @@ namespace Lucene.Net.Benchmarks.ByTask.Tasks
 
         protected virtual ICollector CreateCollector()
         {
-            return TopScoreDocCollector.Create(NumHits, true);
+            return TopScoreDocCollector.Create(NumHits, docsScoredInOrder: true);
         }
 
 
diff --git a/src/Lucene.Net.Benchmark/Quality/Utils/QualityQueriesFinder.cs b/src/Lucene.Net.Benchmark/Quality/Utils/QualityQueriesFinder.cs
index e958b02ec..1592d1734 100644
--- a/src/Lucene.Net.Benchmark/Quality/Utils/QualityQueriesFinder.cs
+++ b/src/Lucene.Net.Benchmark/Quality/Utils/QualityQueriesFinder.cs
@@ -1,5 +1,6 @@
 using Lucene.Net.Index;
 using Lucene.Net.Store;
+using Lucene.Net.Util;
 using System;
 using System.IO;
 using Console = Lucene.Net.Util.SystemConsole;
@@ -94,7 +95,7 @@ namespace Lucene.Net.Benchmarks.Quality.Utils
 
         private string[] BestTerms(string field, int numTerms)
         {
-            Util.PriorityQueue<TermDf> pq = new TermsDfQueue(numTerms);
+            PriorityQueue<TermDf> pq = new TermsDfQueue(numTerms);
             IndexReader ir = DirectoryReader.Open(dir);
             try
             {
@@ -140,10 +141,10 @@ namespace Lucene.Net.Benchmarks.Quality.Utils
             }
         }
 
-        private class TermsDfQueue : Util.PriorityQueue<TermDf>
+        private class TermsDfQueue : PriorityQueue<TermDf>
         {
             internal TermsDfQueue(int maxSize)
-                    : base(maxSize)
+                : base(maxSize)
             {
             }
 
diff --git a/src/Lucene.Net.Facet/TopOrdAndFloatQueue.cs b/src/Lucene.Net.Facet/TopOrdAndFloatQueue.cs
index 61c35a331..d5da14cb9 100644
--- a/src/Lucene.Net.Facet/TopOrdAndFloatQueue.cs
+++ b/src/Lucene.Net.Facet/TopOrdAndFloatQueue.cs
@@ -33,15 +33,26 @@ namespace Lucene.Net.Facet
         // LUCENENET specific - de-nested OrdAndValue and made it into a generic struct
         // so it can be used with this class and TopOrdAndInt32Queue
 
+#nullable enable
+
         /// <summary>
-        /// Sole constructor.
+        /// Initializes a new instance of <see cref="TopOrdAndSingleQueue"/> with the
+        /// specified <paramref name="topN"/> size.
         /// </summary>
-        public TopOrdAndSingleQueue(int topN) : base(topN, false)
+        public TopOrdAndSingleQueue(int topN) : base(topN) // LUCENENET NOTE: Doesn't pre-populate because sentinelFactory is null
         {
         }
 
+#nullable restore
+
         protected internal override bool LessThan(OrdAndValue<float> a, OrdAndValue<float> b)
         {
+            // LUCENENET specific - added guard clauses
+            if (a is null)
+                throw new ArgumentNullException(nameof(a));
+            if (b is null)
+                throw new ArgumentNullException(nameof(b));
+
             if (a.Value < b.Value)
             {
                 return true;
diff --git a/src/Lucene.Net.Facet/TopOrdAndIntQueue.cs b/src/Lucene.Net.Facet/TopOrdAndIntQueue.cs
index f63040c9d..13300bf49 100644
--- a/src/Lucene.Net.Facet/TopOrdAndIntQueue.cs
+++ b/src/Lucene.Net.Facet/TopOrdAndIntQueue.cs
@@ -1,5 +1,6 @@
 // Lucene version compatibility level 4.8.1
 using Lucene.Net.Util;
+using System;
 
 namespace Lucene.Net.Facet
 {
@@ -31,16 +32,27 @@ namespace Lucene.Net.Facet
         // LUCENENET specific - de-nested OrdAndValue and made it into a generic struct
         // so it can be used with this class and TopOrdAndSingleQueue
 
+#nullable enable
+
         /// <summary>
-        /// Sole constructor.
+        /// Initializes a new instance of <see cref="TopOrdAndInt32Queue"/> with the specified
+        /// <paramref name="topN"/> size.
         /// </summary>
         public TopOrdAndInt32Queue(int topN)
-            : base(topN, false)
+            : base(topN) // LUCENENET NOTE: Doesn't pre-populate because sentinelFactory is null
         {
         }
 
+#nullable restore
+
         protected internal override bool LessThan(OrdAndValue<int> a, OrdAndValue<int> b)
         {
+            // LUCENENET specific - added guard clauses
+            if (a is null)
+                throw new ArgumentNullException(nameof(a));
+            if (b is null)
+                throw new ArgumentNullException(nameof(b));
+
             if (a.Value < b.Value)
             {
                 return true;
diff --git a/src/Lucene.Net.Misc/Misc/HighFreqTerms.cs b/src/Lucene.Net.Misc/Misc/HighFreqTerms.cs
index ba18b8cdb..1153f1f21 100644
--- a/src/Lucene.Net.Misc/Misc/HighFreqTerms.cs
+++ b/src/Lucene.Net.Misc/Misc/HighFreqTerms.cs
@@ -191,15 +191,17 @@ namespace Lucene.Net.Misc
         /// Priority queue for <see cref="TermStats"/> objects
         /// 
         /// </summary>
-        internal sealed class TermStatsQueue : Util.PriorityQueue<TermStats>
+        internal sealed class TermStatsQueue : PriorityQueue<TermStats>
         {
             internal readonly IComparer<TermStats> comparer;
 
+#nullable enable
             internal TermStatsQueue(int size, IComparer<TermStats> comparer) 
                 : base(size)
             {
-                this.comparer = comparer;
+                this.comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); // LUCENENET: Added null guard clause
             }
+#nullable restore
 
             protected internal override bool LessThan(TermStats termInfoA, TermStats termInfoB)
             {
diff --git a/src/Lucene.Net.Sandbox/Queries/FuzzyLikeThisQuery.cs b/src/Lucene.Net.Sandbox/Queries/FuzzyLikeThisQuery.cs
index b6b2ad86f..b2aa90e23 100644
--- a/src/Lucene.Net.Sandbox/Queries/FuzzyLikeThisQuery.cs
+++ b/src/Lucene.Net.Sandbox/Queries/FuzzyLikeThisQuery.cs
@@ -352,7 +352,7 @@ namespace Lucene.Net.Sandbox.Queries
             }
         }
 
-        internal class ScoreTermQueue : Util.PriorityQueue<ScoreTerm>
+        internal class ScoreTermQueue : PriorityQueue<ScoreTerm>
         {
             public ScoreTermQueue(int size)
                 : base(size)
@@ -361,7 +361,7 @@ namespace Lucene.Net.Sandbox.Queries
 
             /// <summary>
             /// (non-Javadoc)
-            /// <see cref="Util.PriorityQueue{T}.LessThan(T, T)"/>
+            /// <see cref="PriorityQueue{T}.LessThan(T, T)"/>
             /// </summary>
             protected internal override bool LessThan(ScoreTerm termA, ScoreTerm termB)
             {
diff --git a/src/Lucene.Net.Suggest/Suggest/Lookup.cs b/src/Lucene.Net.Suggest/Suggest/Lookup.cs
index 27440d0ad..a04cb73da 100644
--- a/src/Lucene.Net.Suggest/Suggest/Lookup.cs
+++ b/src/Lucene.Net.Suggest/Suggest/Lookup.cs
@@ -162,17 +162,26 @@ namespace Lucene.Net.Search.Suggest
         /// </summary>
         public sealed class LookupPriorityQueue : PriorityQueue<LookupResult>
         {
+#nullable enable
             // TODO: should we move this out of the interface into a utility class?
             /// <summary>
-            /// Creates a new priority queue of the specified size.
+            /// Creates a new priority queue of the specified <paramref name="size"/>.
             /// </summary>
             public LookupPriorityQueue(int size)
                 : base(size)
             {
             }
 
+#nullable restore
+
             protected internal override bool LessThan(LookupResult a, LookupResult b)
             {
+                // LUCENENET: Added guard clauses
+                if (a is null)
+                    throw new ArgumentNullException(nameof(a));
+                if (b is null)
+                    throw new ArgumentNullException(nameof(b));
+
                 return a.Value < b.Value;
             }
 
diff --git a/src/Lucene.Net.Tests/Util/TestPriorityQueue.cs b/src/Lucene.Net.Tests/Util/TestPriorityQueue.cs
index 2c6add0f7..a99156fc9 100644
--- a/src/Lucene.Net.Tests/Util/TestPriorityQueue.cs
+++ b/src/Lucene.Net.Tests/Util/TestPriorityQueue.cs
@@ -45,10 +45,19 @@ namespace Lucene.Net.Util
             }
 
             public IntegerQueue(int count, bool prepopulate)
-                : base(count, prepopulate)
+                : base(count, prepopulate ? SentinelFactory.Default : null)
             {
             }
 
+            // LUCENENET specific - "prepopulate" is now controlled by whether
+            // or not SentinelFactory is null.
+            private class SentinelFactory : SentinelFactory<int?, IntegerQueue>
+            {
+                public static SentinelFactory Default { get; } = new SentinelFactory();
+
+                public override int? Create(IntegerQueue integerQueue) => int.MaxValue;
+            }
+
             protected internal override bool LessThan(int? a, int? b)
             {
                 return (a < b);
@@ -157,19 +166,6 @@ namespace Lucene.Net.Util
 
         #region LUCENENET SPECIFIC TESTS
 
-        private class IntegerQueueWithSentinel : IntegerQueue
-        {
-            public IntegerQueueWithSentinel(int count, bool prepopulate)
-                : base(count, prepopulate)
-            {
-            }
-
-            protected override int? GetSentinelObject()
-            {
-                return int.MaxValue;
-            }
-        }
-
         private class MyType
         {
             public MyType(int field)
@@ -272,7 +268,7 @@ namespace Lucene.Net.Util
         {
             int maxSize = 10;
             // Populates the internal array
-            PriorityQueue<int?> pq = new IntegerQueueWithSentinel(maxSize, true);
+            PriorityQueue<int?> pq = new IntegerQueue(maxSize, true);
             Assert.AreEqual(pq.Top, int.MaxValue);
             Assert.AreEqual(pq.Count, 10);
 
@@ -435,7 +431,7 @@ namespace Lucene.Net.Util
             
             // Add an element to a prepopulated queue
             int maxSize = 10;
-            PriorityQueue<int?> pq = new IntegerQueueWithSentinel(maxSize, true);
+            PriorityQueue<int?> pq = new IntegerQueue(maxSize, true);
 
             try
             {
diff --git a/src/Lucene.Net/Index/MultiTermsEnum.cs b/src/Lucene.Net/Index/MultiTermsEnum.cs
index 7b2783a43..3b8d20a28 100644
--- a/src/Lucene.Net/Index/MultiTermsEnum.cs
+++ b/src/Lucene.Net/Index/MultiTermsEnum.cs
@@ -79,11 +79,15 @@ namespace Lucene.Net.Index
         public TermsEnumWithSlice[] MatchArray => top;
 
         /// <summary>
-        /// Sole constructor. </summary>
-        /// <param name="slices"> Which sub-reader slices we should
-        /// merge.</param>
+        /// Initializes a new instance of <see cref="MultiTermsEnum"/> with the specified <paramref name="slices"/>. </summary>
+        /// <param name="slices"> Which sub-reader slices we should merge.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="slices"/> is <c>null</c>.</exception>
         public MultiTermsEnum(ReaderSlice[] slices)
         {
+            // LUCENENET: Added guard clause
+            if (slices is null)
+                throw new ArgumentNullException(nameof(slices));
+
             queue = new TermMergeQueue(slices.Length);
             top = new TermsEnumWithSlice[slices.Length];
             subs = new TermsEnumWithSlice[slices.Length];
diff --git a/src/Lucene.Net/Search/FieldValueHitQueue.cs b/src/Lucene.Net/Search/FieldValueHitQueue.cs
index bf18d0da3..7d4e4737e 100644
--- a/src/Lucene.Net/Search/FieldValueHitQueue.cs
+++ b/src/Lucene.Net/Search/FieldValueHitQueue.cs
@@ -1,7 +1,10 @@
 using Lucene.Net.Diagnostics;
+using Lucene.Net.Index;
 using Lucene.Net.Support;
+using Lucene.Net.Util;
 using System;
 using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
 using System.IO;
 
 namespace Lucene.Net.Search
@@ -51,13 +54,14 @@ namespace Lucene.Net.Search
         {
             private readonly int oneReverseMul; // LUCENENET: marked readonly
 
+#nullable enable
             public OneComparerFieldValueHitQueue(SortField[] fields, int size)
                 : base(fields, size)
             {
+                if (fields is null)
+                    throw new ArgumentNullException(nameof(fields)); // LUCENENET: Added guard clause
                 if (fields.Length == 0)
-                {
                     throw new ArgumentException("Sort must contain at least one field");
-                }
 
                 SortField field = fields[0];
                 SetComparer(0, field.GetComparer(size, 0));
@@ -65,6 +69,7 @@ namespace Lucene.Net.Search
 
                 ReverseMul[0] = oneReverseMul;
             }
+#nullable restore
 
             /// <summary> Returns whether <c>a</c> is less relevant than <c>b</c>.</summary>
             /// <param name="hitA">ScoreDoc</param>
@@ -72,6 +77,12 @@ namespace Lucene.Net.Search
             /// <returns><c>true</c> if document <c>a</c> should be sorted after document <c>b</c>.</returns>
             protected internal override bool LessThan(T hitA, T hitB)
             {
+                // LUCENENET specific - added null guard clauses
+                if (hitA is null)
+                    throw new ArgumentNullException(nameof(hitA));
+                if (hitB is null)
+                    throw new ArgumentNullException(nameof(hitB));
+
                 if (Debugging.AssertsEnabled)
                 {
                     Debugging.Assert(hitA != hitB);
@@ -95,9 +106,14 @@ namespace Lucene.Net.Search
         internal sealed class MultiComparersFieldValueHitQueue<T> : FieldValueHitQueue<T>
             where T : FieldValueHitQueue.Entry
         {
+#nullable enable
             public MultiComparersFieldValueHitQueue(SortField[] fields, int size)
                 : base(fields, size)
             {
+                // LUCENENET specific - added null guard clause
+                if (fields is null)
+                    throw new ArgumentNullException(nameof(fields));
+
                 int numComparers = m_comparers.Length;
                 for (int i = 0; i < numComparers; ++i)
                 {
@@ -110,6 +126,12 @@ namespace Lucene.Net.Search
 
             protected internal override bool LessThan(T hitA, T hitB)
             {
+                // LUCENENET specific - added null guard clauses
+                if (hitA is null)
+                    throw new ArgumentNullException(nameof(hitA));
+                if (hitB is null)
+                    throw new ArgumentNullException(nameof(hitB));
+
                 if (Debugging.AssertsEnabled)
                 {
                     Debugging.Assert(hitA != hitB);
@@ -145,6 +167,10 @@ namespace Lucene.Net.Search
         public static FieldValueHitQueue<T> Create<T>(SortField[] fields, int size)
             where T : FieldValueHitQueue.Entry
         {
+            // LUCENENET specific - added null guard clause
+            if (fields is null)
+                throw new ArgumentNullException(nameof(fields));
+
             if (fields.Length == 0)
             {
                 throw new ArgumentException("Sort must contain at least one field");
@@ -159,6 +185,8 @@ namespace Lucene.Net.Search
                 return new FieldValueHitQueue.MultiComparersFieldValueHitQueue<T>(fields, size);
             }
         }
+
+#nullable restore
     }
 
     /// <summary>
@@ -170,9 +198,10 @@ namespace Lucene.Net.Search
     /// @since 2.9 </summary>
     /// <seealso cref="IndexSearcher.Search(Query,Filter,int,Sort)"/>
     /// <seealso cref="FieldCache"/>
-    public abstract class FieldValueHitQueue<T> : Util.PriorityQueue<T>
+    public abstract class FieldValueHitQueue<T> : PriorityQueue<T>
         where T : FieldValueHitQueue.Entry
     {
+#nullable enable
         // prevent instantiation and extension.
         private protected FieldValueHitQueue(SortField[] fields, int size) // LUCENENET: Changed from private to private protected
             : base(size)
@@ -188,6 +217,7 @@ namespace Lucene.Net.Search
             m_comparers = new FieldComparer[numComparers];
             m_reverseMul = new int[numComparers];
         }
+#nullable restore
 
         [WritableArray]
         [SuppressMessage("Microsoft.Performance", "CA1819", Justification = "Lucene's design requires some writable array properties")]
diff --git a/src/Lucene.Net/Search/HitQueue.cs b/src/Lucene.Net/Search/HitQueue.cs
index 0bc01ebc7..5f4c4d046 100644
--- a/src/Lucene.Net/Search/HitQueue.cs
+++ b/src/Lucene.Net/Search/HitQueue.cs
@@ -1,4 +1,8 @@
-namespace Lucene.Net.Search
+using Lucene.Net.Util;
+using System;
+#nullable enable
+
+namespace Lucene.Net.Search
 {
     /*
      * Licensed to the Apache Software Foundation (ASF) under one or more
@@ -17,8 +21,6 @@
      * limitations under the License.
      */
 
-    using Lucene.Net.Util;
-
     internal sealed class HitQueue : PriorityQueue<ScoreDoc>
     {
         /// <summary>
@@ -53,29 +55,44 @@
         /// }
         /// </code>
         ///
-        /// <para/><b>NOTE</b>: this class pre-allocate a full array of
+        /// <para/><b>NOTE</b>: this overload will pre-allocate a full array of
         /// length <paramref name="size"/>.
         /// </summary>
         /// <param name="size">
         ///          The requested size of this queue. </param>
         /// <param name="prePopulate">
         ///          Specifies whether to pre-populate the queue with sentinel values. </param>
-        /// <seealso cref="GetSentinelObject()"/>
+        /// <seealso cref="SentinelFactory"/>
         internal HitQueue(int size, bool prePopulate)
-            : base(size, prePopulate)
+            : base(size, prePopulate ? SentinelFactory.Default : null)
         {
         }
 
-        protected override ScoreDoc GetSentinelObject()
+        // LUCENENET specific - Rather than having a GetSentinelObject() method on PriorityQueue<T>,
+        // and a "prePopulate" boolean value, population is controlled by whether ISentinelFactory<T>
+        // has an instance or is null. This is the singleton instance that is injected when "prePopulate"
+        // is true.
+        internal sealed class SentinelFactory : SentinelFactory<ScoreDoc, HitQueue>
         {
-            // Always set the doc Id to MAX_VALUE so that it won't be favored by
-            // lessThan. this generally should not happen since if score is not NEG_INF,
-            // TopScoreDocCollector will always add the object to the queue.
-            return new ScoreDoc(int.MaxValue, float.NegativeInfinity);
+            public static SentinelFactory Default { get; } = new SentinelFactory();
+
+            public override ScoreDoc Create(HitQueue priorityQueue)
+            {
+                // Always set the doc Id to MAX_VALUE so that it won't be favored by
+                // lessThan. this generally should not happen since if score is not NEG_INF,
+                // TopScoreDocCollector will always add the object to the queue.
+                return new ScoreDoc(int.MaxValue, float.NegativeInfinity);
+            }
         }
 
         protected internal override sealed bool LessThan(ScoreDoc hitA, ScoreDoc hitB)
         {
+            // LUCENENET: Added guard clauses
+            if (hitA is null)
+                throw new ArgumentNullException(nameof(hitA));
+            if (hitB is null)
+                throw new ArgumentNullException(nameof(hitB));
+
             // LUCENENET specific - compare bits rather than using equality operators to prevent these comparisons from failing in x86 in .NET Framework with optimizations enabled
             if (NumericUtils.SingleToSortableInt32(hitA.Score) == NumericUtils.SingleToSortableInt32(hitB.Score))
             {
diff --git a/src/Lucene.Net/Search/IndexSearcher.cs b/src/Lucene.Net/Search/IndexSearcher.cs
index 211029752..abdee3c17 100644
--- a/src/Lucene.Net/Search/IndexSearcher.cs
+++ b/src/Lucene.Net/Search/IndexSearcher.cs
@@ -460,7 +460,7 @@ namespace Lucene.Net.Search
             }
             else
             {
-                HitQueue hq = new HitQueue(nDocs, false);
+                HitQueue hq = new HitQueue(nDocs, prePopulate: false);
                 ReentrantLock @lock = new ReentrantLock();
                 ExecutionHelper<TopDocs> runner = new ExecutionHelper<TopDocs>(executor);
 
@@ -538,7 +538,7 @@ namespace Lucene.Net.Search
         {
             if (sort is null)
             {
-                throw new ArgumentNullException("Sort must not be null"); // LUCENENET specific - changed from IllegalArgumentException to ArgumentNullException (.NET convention)
+                throw new ArgumentNullException(nameof(sort), "Sort must not be null"); // LUCENENET specific - changed from IllegalArgumentException to ArgumentNullException (.NET convention)
             }
 
             int limit = reader.MaxDoc;
diff --git a/src/Lucene.Net/Search/Spans/SpanOrQuery.cs b/src/Lucene.Net/Search/Spans/SpanOrQuery.cs
index 7ba55ce17..9753f5f2e 100644
--- a/src/Lucene.Net/Search/Spans/SpanOrQuery.cs
+++ b/src/Lucene.Net/Search/Spans/SpanOrQuery.cs
@@ -177,7 +177,7 @@ namespace Lucene.Net.Search.Spans
             return h;
         }
 
-        private class SpanQueue : Util.PriorityQueue<Spans>
+        private class SpanQueue : PriorityQueue<Spans>
         {
             public SpanQueue(int size)
                 : base(size)
diff --git a/src/Lucene.Net/Search/TopDocs.cs b/src/Lucene.Net/Search/TopDocs.cs
index 160f00949..7930ef355 100644
--- a/src/Lucene.Net/Search/TopDocs.cs
+++ b/src/Lucene.Net/Search/TopDocs.cs
@@ -89,15 +89,20 @@ namespace Lucene.Net.Search
             }
         }
 
+#nullable enable
+
         // Specialized MergeSortQueue that just merges by
         // relevance score, descending:
-        private class ScoreMergeSortQueue : Util.PriorityQueue<ShardRef>
+        private sealed class ScoreMergeSortQueue : PriorityQueue<ShardRef> // LUCENENET specific - marked sealed
         {
             internal readonly ScoreDoc[][] shardHits;
 
             public ScoreMergeSortQueue(TopDocs[] shardHits)
-                : base(shardHits.Length)
+                : base(shardHits?.Length ?? 0)
             {
+                if (shardHits is null)
+                    throw new ArgumentNullException(nameof(shardHits));
+
                 this.shardHits = new ScoreDoc[shardHits.Length][];
                 for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++)
                 {
@@ -108,6 +113,12 @@ namespace Lucene.Net.Search
             // Returns true if first is < second
             protected internal override bool LessThan(ShardRef first, ShardRef second)
             {
+                // LUCENENET specific - added guard clauses
+                if (first is null)
+                    throw new ArgumentNullException(nameof(first));
+                if (second is null)
+                    throw new ArgumentNullException(nameof(second));
+
                 if (Debugging.AssertsEnabled) Debugging.Assert(first != second);
                 float firstScore = shardHits[first.ShardIndex][first.HitIndex].Score;
                 float secondScore = shardHits[second.ShardIndex][second.HitIndex].Score;
@@ -143,7 +154,7 @@ namespace Lucene.Net.Search
             }
         }
 
-        private class MergeSortQueue : Util.PriorityQueue<ShardRef>
+        private sealed class MergeSortQueue : PriorityQueue<ShardRef> // LUCENENET specific - marked sealed
         {
             // These are really FieldDoc instances:
             internal readonly ScoreDoc[][] shardHits;
@@ -152,8 +163,11 @@ namespace Lucene.Net.Search
             internal readonly int[] reverseMul;
 
             public MergeSortQueue(Sort sort, TopDocs[] shardHits)
-                : base(shardHits.Length)
+                : base(shardHits?.Length ?? 0)
             {
+                if (shardHits is null)
+                    throw new ArgumentNullException(nameof(shardHits));
+
                 this.shardHits = new ScoreDoc[shardHits.Length][];
                 for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++)
                 {
@@ -190,9 +204,17 @@ namespace Lucene.Net.Search
                 }
             }
 
+
+
             // Returns true if first is < second
             protected internal override bool LessThan(ShardRef first, ShardRef second)
             {
+                // LUCENENET specific - added guard clauses
+                if (first is null)
+                    throw new ArgumentNullException(nameof(first));
+                if (second is null)
+                    throw new ArgumentNullException(nameof(second));
+
                 if (Debugging.AssertsEnabled) Debugging.Assert(first != second);
                 FieldDoc firstFD = (FieldDoc)shardHits[first.ShardIndex][first.HitIndex];
                 FieldDoc secondFD = (FieldDoc)shardHits[second.ShardIndex][second.HitIndex];
@@ -247,8 +269,9 @@ namespace Lucene.Net.Search
         /// <para/>
         /// @lucene.experimental
         /// </summary>
+        /// <exception cref="ArgumentNullException"><paramref name="shardHits"/> is <c>null</c>.</exception>
         [MethodImpl(MethodImplOptions.NoInlining)]
-        public static TopDocs Merge(Sort sort, int topN, TopDocs[] shardHits)
+        public static TopDocs Merge(Sort? sort, int topN, TopDocs[] shardHits)
         {
             return Merge(sort, 0, topN, shardHits);
         }
@@ -258,10 +281,15 @@ namespace Lucene.Net.Search
         /// on the provided start and size. The return <c>TopDocs</c> will always have a scoreDocs with length of 
         /// at most <see cref="Util.PriorityQueue{T}.Count"/>.
         /// </summary>
+        /// <exception cref="ArgumentNullException"><paramref name="shardHits"/> is <c>null</c>.</exception>
         [MethodImpl(MethodImplOptions.NoInlining)]
-        public static TopDocs Merge(Sort sort, int start, int size, TopDocs[] shardHits)
+        public static TopDocs Merge(Sort? sort, int start, int size, TopDocs[] shardHits)
         {
-            Util.PriorityQueue<ShardRef> queue;
+            // LUCENENET specific - added guard clause.
+            if (shardHits is null)
+                throw new ArgumentNullException(nameof(shardHits));
+
+            PriorityQueue<ShardRef> queue;
             if (sort is null)
             {
                 queue = new ScoreMergeSortQueue(shardHits);
diff --git a/src/Lucene.Net/Search/TopFieldCollector.cs b/src/Lucene.Net/Search/TopFieldCollector.cs
index 951fc9194..c666fca37 100644
--- a/src/Lucene.Net/Search/TopFieldCollector.cs
+++ b/src/Lucene.Net/Search/TopFieldCollector.cs
@@ -1161,6 +1161,8 @@ namespace Lucene.Net.Search
             this.fillFields = fillFields;
         }
 
+#nullable enable
+
         /// <summary>
         /// Creates a new <see cref="TopFieldCollector"/> from the given
         /// arguments.
@@ -1196,9 +1198,10 @@ namespace Lucene.Net.Search
         /// <returns> A <see cref="TopFieldCollector"/> instance which will sort the results by
         ///         the sort criteria. </returns>
         /// <exception cref="IOException"> If there is a low-level I/O error </exception>
+        /// <exception cref="ArgumentNullException"><paramref name="sort"/> is <c>null</c>.</exception>
         public static TopFieldCollector Create(Sort sort, int numHits, bool fillFields, bool trackDocScores, bool trackMaxScore, bool docsScoredInOrder)
         {
-            return Create(sort, numHits, null, fillFields, trackDocScores, trackMaxScore, docsScoredInOrder);
+            return Create(sort, numHits, after: null, fillFields, trackDocScores, trackMaxScore, docsScoredInOrder);
         }
 
         /// <summary>
@@ -1238,7 +1241,8 @@ namespace Lucene.Net.Search
         /// <returns> A <see cref="TopFieldCollector"/> instance which will sort the results by
         ///         the sort criteria. </returns>
         /// <exception cref="IOException"> If there is a low-level I/O error </exception>
-        public static TopFieldCollector Create(Sort sort, int numHits, FieldDoc after, bool fillFields, bool trackDocScores, bool trackMaxScore, bool docsScoredInOrder)
+        /// <exception cref="ArgumentNullException"><paramref name="sort"/> is <c>null</c>.</exception>
+        public static TopFieldCollector Create(Sort sort, int numHits, FieldDoc? after, bool fillFields, bool trackDocScores, bool trackMaxScore, bool docsScoredInOrder)
         {
             // LUCENENET specific: Added guard clause for null
             if (sort is null)
@@ -1353,6 +1357,10 @@ namespace Lucene.Net.Search
 
         protected override void PopulateResults(ScoreDoc[] results, int howMany)
         {
+            // LUCENENET specific - Added guard clause
+            if (results is null)
+                throw new ArgumentNullException(nameof(results));
+
             if (fillFields)
             {
                 // avoid casting if unnecessary.
@@ -1372,7 +1380,7 @@ namespace Lucene.Net.Search
             }
         }
 
-        protected override TopDocs NewTopDocs(ScoreDoc[] results, int start)
+        protected override TopDocs NewTopDocs(ScoreDoc[]? results, int start)
         {
             if (results is null)
             {
diff --git a/src/Lucene.Net/Search/TopScoreDocCollector.cs b/src/Lucene.Net/Search/TopScoreDocCollector.cs
index f24bb51f3..a0ee5149e 100644
--- a/src/Lucene.Net/Search/TopScoreDocCollector.cs
+++ b/src/Lucene.Net/Search/TopScoreDocCollector.cs
@@ -2,6 +2,7 @@
 using Lucene.Net.Support;
 using Lucene.Net.Util;
 using System;
+using System.Globalization;
 
 namespace Lucene.Net.Search
 {
@@ -40,12 +41,14 @@ namespace Lucene.Net.Search
     public abstract class TopScoreDocCollector : TopDocsCollector<ScoreDoc>
     {
         // Assumes docs are scored in order.
-        private class InOrderTopScoreDocCollector : TopScoreDocCollector
+        private sealed class InOrderTopScoreDocCollector : TopScoreDocCollector // LUCENENET specific - marked sealed
         {
+#nullable enable
             internal InOrderTopScoreDocCollector(int numHits)
                 : base(numHits)
             {
             }
+#nullable restore
 
             public override void Collect(int doc)
             {
@@ -76,7 +79,7 @@ namespace Lucene.Net.Search
         }
 
         // Assumes docs are scored in order.
-        private class InOrderPagingScoreDocCollector : TopScoreDocCollector
+        private sealed class InOrderPagingScoreDocCollector : TopScoreDocCollector // LUCENENET specific - marked sealed
         {
             internal readonly ScoreDoc after;
 
@@ -84,13 +87,13 @@ namespace Lucene.Net.Search
             internal int afterDoc;
 
             internal int collectedHits;
-
+#nullable enable
             internal InOrderPagingScoreDocCollector(ScoreDoc after, int numHits)
                 : base(numHits)
             {
                 this.after = after;
             }
-
+#nullable restore
             public override void Collect(int doc)
             {
                 float score = scorer.GetScore();
@@ -145,13 +148,17 @@ namespace Lucene.Net.Search
         }
 
         // Assumes docs are scored out of order.
-        private class OutOfOrderTopScoreDocCollector : TopScoreDocCollector
+        private sealed class OutOfOrderTopScoreDocCollector : TopScoreDocCollector // LUCENENET specific - marked sealed
         {
+#nullable enable
+
             internal OutOfOrderTopScoreDocCollector(int numHits)
                 : base(numHits)
             {
             }
 
+#nullable restore
+
             public override void Collect(int doc)
             {
                 float score = scorer.GetScore();
@@ -184,7 +191,7 @@ namespace Lucene.Net.Search
         }
 
         // Assumes docs are scored out of order.
-        private class OutOfOrderPagingScoreDocCollector : TopScoreDocCollector
+        private sealed class OutOfOrderPagingScoreDocCollector : TopScoreDocCollector // LUCENENET specific - marked sealed
         {
             internal readonly ScoreDoc after;
 
@@ -192,12 +199,13 @@ namespace Lucene.Net.Search
             internal int afterDoc;
 
             internal int collectedHits;
-
+#nullable enable
             internal OutOfOrderPagingScoreDocCollector(ScoreDoc after, int numHits)
                 : base(numHits)
             {
                 this.after = after;
             }
+#nullable restore
 
             public override void Collect(int doc)
             {
@@ -250,6 +258,8 @@ namespace Lucene.Net.Search
             }
         }
 
+#nullable enable
+
         /// <summary>
         /// Creates a new <see cref="TopScoreDocCollector"/> given the number of hits to
         /// collect and whether documents are scored in order by the input
@@ -262,7 +272,7 @@ namespace Lucene.Net.Search
         /// </summary>
         public static TopScoreDocCollector Create(int numHits, bool docsScoredInOrder)
         {
-            return Create(numHits, null, docsScoredInOrder);
+            return Create(numHits, after: null, docsScoredInOrder);
         }
 
         /// <summary>
@@ -275,7 +285,7 @@ namespace Lucene.Net.Search
         /// <paramref name="numHits"/>, and fill the array with sentinel
         /// objects.
         /// </summary>
-        public static TopScoreDocCollector Create(int numHits, ScoreDoc after, bool docsScoredInOrder)
+        public static TopScoreDocCollector Create(int numHits, ScoreDoc? after, bool docsScoredInOrder)
         {
             if (numHits <= 0)
             {
@@ -284,28 +294,29 @@ namespace Lucene.Net.Search
 
             if (docsScoredInOrder)
             {
-                return after is null ? (TopScoreDocCollector)new InOrderTopScoreDocCollector(numHits) : new InOrderPagingScoreDocCollector(after, numHits);
+                return after is null ? new InOrderTopScoreDocCollector(numHits) : new InOrderPagingScoreDocCollector(after, numHits);
             }
             else
             {
-                return after is null ? (TopScoreDocCollector)new OutOfOrderTopScoreDocCollector(numHits) : new OutOfOrderPagingScoreDocCollector(after, numHits);
+                return after is null ? new OutOfOrderTopScoreDocCollector(numHits) : new OutOfOrderPagingScoreDocCollector(after, numHits);
             }
         }
 
         internal ScoreDoc pqTop;
         internal int docBase = 0;
-        internal Scorer scorer;
+        internal Scorer? scorer;
 
         // prevents instantiation
         private TopScoreDocCollector(int numHits)
-            : base(new HitQueue(numHits, true))
+            : base(new HitQueue(numHits, prePopulate: true))
         {
-            // HitQueue implements getSentinelObject to return a ScoreDoc, so we know
-            // that at this point top() is already initialized.
-            pqTop = m_pq.Top;
+            // HitQueue.SentinelFactory implements Create() to return a ScoreDoc, so we know
+            // that at this point Top is already initialized.
+            pqTop = m_pq.Top!;
         }
 
-        protected override TopDocs NewTopDocs(ScoreDoc[] results, int start)
+
+        protected override TopDocs NewTopDocs(ScoreDoc[]? results, int start)
         {
             if (results is null)
             {
@@ -327,7 +338,7 @@ namespace Lucene.Net.Search
                 {
                     m_pq.Pop();
                 }
-                maxScore = m_pq.Pop().Score;
+                maxScore = m_pq.Pop()!.Score;
             }
 
             return new TopDocs(m_totalHits, results, maxScore);
@@ -335,6 +346,10 @@ namespace Lucene.Net.Search
 
         public override void SetNextReader(AtomicReaderContext context)
         {
+            // LUCENENET: Added guard clause
+            if (context is null)
+                throw new ArgumentNullException(nameof(context));
+
             docBase = context.DocBase;
         }
 
diff --git a/src/Lucene.Net/Util/Fst/FST.cs b/src/Lucene.Net/Util/Fst/FST.cs
index b8dcad18b..4807a2211 100644
--- a/src/Lucene.Net/Util/Fst/FST.cs
+++ b/src/Lucene.Net/Util/Fst/FST.cs
@@ -2321,7 +2321,7 @@ namespace Lucene.Net.Util.Fst
             }
         }
 
-        internal class NodeAndInCount : IComparable<NodeAndInCount>
+        internal class NodeAndInCount : IComparable<NodeAndInCount> // LUCENENET TODO: This is a candidate for a struct
         {
             internal int Node { get; private set; }
             internal int Count { get; private set; }
@@ -2350,16 +2350,23 @@ namespace Lucene.Net.Util.Fst
             }
         }
 
+#nullable enable
         internal class NodeQueue : PriorityQueue<NodeAndInCount>
         {
             public NodeQueue(int topN)
-                : base(topN, false)
+                : base(topN) // LUCENENET NOTE: Doesn't pre-populate because sentinelFactory is null
             {
             }
 
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
             protected internal override bool LessThan(NodeAndInCount a, NodeAndInCount b)
             {
+                // LUCENENET specific - added guard clauses
+                if (a is null)
+                    throw new ArgumentNullException(nameof(a));
+                if (b is null)
+                    throw new ArgumentNullException(nameof(b));
+
                 int cmp = a.CompareTo(b);
                 if (Debugging.AssertsEnabled) Debugging.Assert(cmp != 0);
                 return cmp < 0;
diff --git a/src/Lucene.Net/Util/OfflineSorter.cs b/src/Lucene.Net/Util/OfflineSorter.cs
index 5e0d0ac18..0644bb70a 100644
--- a/src/Lucene.Net/Util/OfflineSorter.cs
+++ b/src/Lucene.Net/Util/OfflineSorter.cs
@@ -547,6 +547,12 @@ namespace Lucene.Net.Util
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
             protected internal override bool LessThan(FileAndTop a, FileAndTop b)
             {
+                // LUCENENET: Added guard clauses
+                if (a is null)
+                    throw new ArgumentNullException(nameof(a));
+                if (b is null)
+                    throw new ArgumentNullException(nameof(b));
+
                 return outerInstance.comparer.Compare(a.Current, b.Current) < 0;
             }
         }
diff --git a/src/Lucene.Net/Util/PriorityQueue.cs b/src/Lucene.Net/Util/PriorityQueue.cs
index 9912fd7cf..7ae0bc590 100644
--- a/src/Lucene.Net/Util/PriorityQueue.cs
+++ b/src/Lucene.Net/Util/PriorityQueue.cs
@@ -1,9 +1,9 @@
 using J2N.Numerics;
 using Lucene.Net.Support;
 using System;
-using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.CompilerServices;
+#nullable enable
 
 namespace Lucene.Net.Util
 {
@@ -24,16 +24,136 @@ namespace Lucene.Net.Util
      * limitations under the License.
      */
 
+    /// <summary>
+    /// Provides the sentinel instances of <typeparamref name="T"/> to a
+    /// <see cref="PriorityQueue{T}"/>.
+    /// </summary>
+    /// <remarks>
+    /// This interface can be implemented to provide sentinel
+    /// instances. The implementation of this interface can be passed to
+    /// a constructor of <see cref="PriorityQueue{T}"/>. If an instance
+    /// is provided, the <see cref="PriorityQueue{T}"/> constructor will use
+    /// the <see cref="Create{TQueue}(TQueue)"/> method to fill the queue,
+    /// so that code which uses the queue can always assume it is full and only
+    /// change the top without attempting to insert any new items.
+    /// <para/>
+    /// Those sentinel values should always compare worse than any non-sentinel
+    /// value (i.e., <see cref="PriorityQueue{T}.LessThan(T, T)"/> should always favor the
+    /// non-sentinel values).
+    /// <para/>
+    /// When using a <see cref="ISentinelFactory{T}"/>, the following usage pattern
+    /// is recommended:
+    /// <code>
+    /// // Implements ISentinelFactory&lt;T&gt;.Create(PriorityQueue&lt;T&gt;)
+    /// var sentinelFactory = new MySentinelFactory&lt;MyObject&gt;();
+    /// PriorityQueue&lt;MyObject&gt; pq = new MyQueue&lt;MyObject&gt;(sentinelFactory);
+    /// // save the 'top' element, which is guaranteed to not be <c>default</c>.
+    /// MyObject pqTop = pq.Top;
+    /// &lt;...&gt;
+    /// // now in order to add a new element, which is 'better' than top (after
+    /// // you've verified it is better), it is as simple as:
+    /// pqTop.Change();
+    /// pqTop = pq.UpdateTop();
+    /// </code>
+    /// <b>NOTE:</b> <see cref="Create{TQueue}(TQueue)"/> will be called by the
+    /// <see cref="PriorityQueue{T}"/> constructor <see cref="PriorityQueue{T}.Count"/>
+    /// times, relying on a new instance to be returned. Therefore you should ensure any call to this
+    /// <see cref="Create{TQueue}(TQueue)"/> creates a new instance and behaves consistently, e.g., it cannot
+    /// return <c>null</c> if it previously returned non-<c>null</c>.
+    /// <para/>
+    /// To make implementing this interface easier, it is recommended to use the
+    /// <see cref="SentinelFactory{T, TPriorityQueue}"/> abstract class.
+    /// </remarks>
+    /// <typeparam name="T">The type of sentinel instance to create.</typeparam>
+    /// <seealso cref="SentinelFactory{T, TPriorityQueue}"/>
+    // LUCENENET specific interface to eliminate the virtual method call in the PriorityQueue<T> constructor.
+    public interface ISentinelFactory<T>
+    {
+        /// <summary>
+        /// Creates a sentinel instance of <typeparamref name="T"/> to fill an element
+        /// of a <see cref="PriorityQueue{T}"/>.
+        /// </summary>
+        /// <typeparam name="TQueue">The type of priority queue that is calling this method.</typeparam>
+        /// <param name="priorityQueue">The <see cref="PriorityQueue{T}"/> instance that is calling this method.
+        /// <para/>
+        /// <b>NOTE:</b> The call to this method happens in the constructor, so it occurs prior to any
+        /// subclass construtor state being set. If you need to access state from your subclass, you should
+        /// pass that state into the constructor of the implementation of this interface.</param>
+        /// <returns>A newly created sentinel instance for use in a single element of <see cref="PriorityQueue{T}"/>.</returns>
+        T Create<TQueue>(TQueue priorityQueue) where TQueue : PriorityQueue<T>;
+    }
+
+    /// <summary>
+    /// Provides the sentinel instances of <typeparamref name="T"/> to a
+    /// <see cref="PriorityQueue{T}"/>.
+    /// </summary>
+    /// <remarks>
+    /// This class can be extended to provide sentinel
+    /// instances. The concrete class instance can be passed to
+    /// a constructor of <see cref="PriorityQueue{T}"/>. If an instance
+    /// is provided, the <see cref="PriorityQueue{T}"/> constructor will use
+    /// the <see cref="Create(TPriorityQueue)"/> method to fill the queue,
+    /// so that code which uses the queue can always assume it is full and only
+    /// change the top without attempting to insert any new items.
+    /// <para/>
+    /// Those sentinel values should always compare worse than any non-sentinel
+    /// value (i.e., <see cref="PriorityQueue{T}.LessThan(T, T)"/> should always favor the
+    /// non-sentinel values).
+    /// <para/>
+    /// When using a <see cref="ISentinelFactory{T}"/>, the following usage pattern
+    /// is recommended:
+    /// <code>
+    /// // Implements ISentinelFactory&lt;T&gt;.Create(PriorityQueue&lt;T&gt;)
+    /// var sentinelFactory = new MySentinelFactory&lt;MyObject&gt;();
+    /// PriorityQueue&lt;MyObject&gt; pq = new MyQueue&lt;MyObject&gt;(sentinelFactory);
+    /// // save the 'top' element, which is guaranteed to not be <c>default</c>.
+    /// MyObject pqTop = pq.Top;
+    /// &lt;...&gt;
+    /// // now in order to add a new element, which is 'better' than top (after
+    /// // you've verified it is better), it is as simple as:
+    /// pqTop.Change();
+    /// pqTop = pq.UpdateTop();
+    /// </code>
+    /// <b>NOTE:</b> <see cref="Create(TPriorityQueue)"/> will be called by the
+    /// <see cref="PriorityQueue{T}"/> constructor <see cref="PriorityQueue{T}.Count"/>
+    /// times, relying on a new instance to be returned. Therefore you should ensure any call to this
+    /// <see cref="Create(TPriorityQueue)"/> creates a new instance and behaves consistently, e.g., it cannot
+    /// return <c>null</c> if it previously returned non-<c>null</c>.
+    /// </remarks>
+    /// <typeparam name="T">The type of sentinel instance to create.</typeparam>
+    /// <typeparam name="TPriorityQueue"></typeparam>
+    // LUCENENET specific class to eliminate the virtual method call in the PriorityQueue<T> constructor.
+    public abstract class SentinelFactory<T, TPriorityQueue> : ISentinelFactory<T>
+        where TPriorityQueue : PriorityQueue<T>
+    {
+        /// <summary>
+        /// Creates a sentinel instance of <typeparamref name="T"/> to fill an element
+        /// of a <see cref="PriorityQueue{T}"/>.
+        /// </summary>
+        /// <param name="priorityQueue">The <see cref="PriorityQueue{T}"/> instance that is calling this method.
+        /// <para/>
+        /// <b>NOTE:</b> The call to this method happens in the constructor, so it occurs prior to any
+        /// subclass construtor state being set. If you need to access state from your subclass, you should
+        /// pass that state into the constructor of the implementation of this interface.</param>
+        /// <returns></returns>
+        public abstract T Create(TPriorityQueue priorityQueue);
+        T ISentinelFactory<T>.Create<TQueue>(TQueue priorityQueue)
+            => Create((TPriorityQueue)(object)priorityQueue);
+    }
+
     /// <summary>
     /// A <see cref="PriorityQueue{T}"/> maintains a partial ordering of its elements such that the
     /// element with least priority can always be found in constant time. Put()'s and Pop()'s
     /// require log(size) time.
     ///
     /// <para/><b>NOTE</b>: this class will pre-allocate a full array of
-    /// length <c>maxSize+1</c> if instantiated via the
-    /// <see cref="PriorityQueue(int, bool)"/> constructor with
-    /// <c>prepopulate</c> set to <c>true</c>. That maximum
-    /// size can grow as we insert elements over the time.
+    /// length <c>maxSize+1</c> if instantiated with a constructor that
+    /// accepts a <see cref="ISentinelFactory{T}"/>. That maximum size can
+    /// grow as we insert elements over time.
+    /// <para/>
+    /// <b>NOTE</b>: The type of <typeparamref name="T"/> must be either a class
+    /// or a nullable value type. Non-nullable value types are not supported and may
+    /// produce undefined behavior.
     /// <para/>
     /// @lucene.internal
     /// </summary>
@@ -44,14 +164,29 @@ namespace Lucene.Net.Util
     {
         private int size = 0;
         internal readonly int maxSize; // LUCENENET: Internal for testing
-        internal readonly T[] heap; // LUCENENET: Internal for testing
+        internal T?[] heap; // LUCENENET: Internal for testing
 
+        /// <summary>
+        /// Initializes a new instance of <see cref="PriorityQueue{T}"/> with the
+        /// specified <paramref name="maxSize"/>.
+        /// </summary>
+        /// <param name="maxSize">The maximum number of elements this queue can hold.</param>
         protected PriorityQueue(int maxSize) // LUCENENET specific - made protected instead of public
-            : this(maxSize, true)
+            : this(maxSize, sentinelFactory: null)
         {
         }
 
-        protected PriorityQueue(int maxSize, bool prepopulate) // LUCENENET specific - made protected instead of public
+        /// <summary>
+        /// Initializes a new instance of <see cref="PriorityQueue{T}"/> with the
+        /// specified <paramref name="maxSize"/> and <paramref name="sentinelFactory"/>.
+        /// </summary>
+        /// <param name="maxSize">The maximum number of elements this queue can hold.</param>
+        /// <param name="sentinelFactory">If not <c>null</c>, the queue will be pre-populated.
+        /// This factory will be called <paramref name="maxSize"/> times to get an instance
+        /// to provide to each element in the queue.</param>
+        /// <seealso cref="ISentinelFactory{T}"/>
+        /// <seealso cref="SentinelFactory{T, TPriorityQueue}"/>
+        protected PriorityQueue(int maxSize, ISentinelFactory<T>? sentinelFactory)
         {
             int heapSize;
             if (0 == maxSize)
@@ -81,24 +216,17 @@ namespace Lucene.Net.Util
                     heapSize = maxSize + 1;
                 }
             }
-            // T is unbounded type, so this unchecked cast works always:
-            T[] h = new T[heapSize];
-            this.heap = h;
             this.maxSize = maxSize;
-
-            if (prepopulate)
+            this.heap = new T[heapSize];
+            // LUCENENET: If we have a sentinel factory, it means we should pre-populate the array
+            // with sentinel values.
+            if (sentinelFactory is not null)
             {
-                // If sentinel objects are supported, populate the queue with them
-                T sentinel = GetSentinelObject();
-                if (!EqualityComparer<T>.Default.Equals(sentinel, default))
+                for (int i = 1; i < heap.Length; i++)
                 {
-                    heap[1] = sentinel;
-                    for (int i = 2; i < heap.Length; i++)
-                    {
-                        heap[i] = GetSentinelObject();
-                    }
-                    size = maxSize;
+                    heap[i] = sentinelFactory.Create(this);
                 }
+                size = maxSize;
             }
         }
 
@@ -108,50 +236,10 @@ namespace Lucene.Net.Util
         /// <returns> <c>true</c> if parameter <paramref name="a"/> is less than parameter <paramref name="b"/>. </returns>
         protected internal abstract bool LessThan(T a, T b); // LUCENENET: Internal for testing
 
-        /// <summary>
-        /// This method can be overridden by extending classes to return a sentinel
-        /// object which will be used by the <see cref="PriorityQueue(int, bool)"/>
-        /// constructor to fill the queue, so that the code which uses that queue can always
-        /// assume it's full and only change the top without attempting to insert any new
-        /// object.
-        /// <para/>
-        /// Those sentinel values should always compare worse than any non-sentinel
-        /// value (i.e., <see cref="LessThan(T, T)"/> should always favor the
-        /// non-sentinel values).
-        /// <para/>
-        /// By default, this method returns <c>false</c>, which means the queue will not be
-        /// filled with sentinel values. Otherwise, the value returned will be used to
-        /// pre-populate the queue. Adds sentinel values to the queue.
-        /// <para/>
-        /// If this method is extended to return a non-null value, then the following
-        /// usage pattern is recommended:
-        ///
-        /// <code>
-        /// // extends GetSentinelObject() to return a non-null value.
-        /// PriorityQueue&lt;MyObject&gt; pq = new MyQueue&lt;MyObject&gt;(numHits);
-        /// // save the 'top' element, which is guaranteed to not be null.
-        /// MyObject pqTop = pq.Top;
-        /// &lt;...&gt;
-        /// // now in order to add a new element, which is 'better' than top (after
-        /// // you've verified it is better), it is as simple as:
-        /// pqTop.Change().
-        /// pqTop = pq.UpdateTop();
-        /// </code>
-        /// <para/>
-        /// <b>NOTE:</b> if this method returns a non-<c>null</c> value, it will be called by
-        /// the <see cref="PriorityQueue(int, bool)"/> constructor
-        /// <see cref="Count"/> times, relying on a new object to be returned and will not
-        /// check if it's <c>null</c> again. Therefore you should ensure any call to this
-        /// method creates a new instance and behaves consistently, e.g., it cannot
-        /// return <c>null</c> if it previously returned non-<c>null</c>.
-        /// </summary>
-        /// <returns> The sentinel object to use to pre-populate the queue, or <c>null</c> if
-        ///         sentinel objects are not supported. </returns>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        protected virtual T GetSentinelObject()
-        {
-            return default;
-        }
+        // LUCENENET specific - refactored getSentinelObject() method into ISentinelFactory<T> and
+        // SentinelFactory<T, TPriorityQueue>
+
+#nullable restore
 
         /// <summary>
         /// Adds an Object to a <see cref="PriorityQueue{T}"/> in log(size) time. If one tries to add
@@ -327,12 +415,12 @@ namespace Lucene.Net.Util
         }
 
         /// <summary>
-        /// This method returns the internal heap array as T[].
+        /// Gets the internal heap array as T[].
         /// <para/>
         /// @lucene.internal
         /// </summary>
         [WritableArray]
         [SuppressMessage("Microsoft.Performance", "CA1819", Justification = "Lucene's design requires some writable array properties")]
-        protected T[] HeapArray => heap;
+        protected internal T[] HeapArray => heap;
     }
 }
\ No newline at end of file
diff --git a/src/Lucene.Net/Util/WAH8DocIdSet.cs b/src/Lucene.Net/Util/WAH8DocIdSet.cs
index 8bb85057b..acbabd345 100644
--- a/src/Lucene.Net/Util/WAH8DocIdSet.cs
+++ b/src/Lucene.Net/Util/WAH8DocIdSet.cs
@@ -3,7 +3,6 @@ using Lucene.Net.Diagnostics;
 using Lucene.Net.Support;
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
 using System.Runtime.CompilerServices;
 
@@ -106,6 +105,7 @@ namespace Lucene.Net.Util
 
         /// <summary>
         /// Same as <see cref="Intersect(ICollection{WAH8DocIdSet}, int)"/> with the default index interval. </summary>
+        /// <exception cref="ArgumentNullException"><paramref name="docIdSets"/> is <c>null</c>.</exception>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static WAH8DocIdSet Intersect(ICollection<WAH8DocIdSet> docIdSets)
         {
@@ -116,6 +116,7 @@ namespace Lucene.Net.Util
         /// Compute the intersection of the provided sets. This method is much faster than
         /// computing the intersection manually since it operates directly at the byte level.
         /// </summary>
+        /// <exception cref="ArgumentNullException"><paramref name="docIdSets"/> is <c>null</c>.</exception>
         public static WAH8DocIdSet Intersect(ICollection<WAH8DocIdSet> docIdSets, int indexInterval)
         {
             // LUCENENET: Added guard clause for null
@@ -183,6 +184,7 @@ namespace Lucene.Net.Util
 
         /// <summary>
         /// Same as <see cref="Union(ICollection{WAH8DocIdSet}, int)"/> with the default index interval. </summary>
+        /// <exception cref="ArgumentNullException"><paramref name="docIdSets"/> is <c>null</c>.</exception>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static WAH8DocIdSet Union(ICollection<WAH8DocIdSet> docIdSets)
         {
@@ -193,8 +195,13 @@ namespace Lucene.Net.Util
         /// Compute the union of the provided sets. This method is much faster than
         /// computing the union manually since it operates directly at the byte level.
         /// </summary>
+        /// <exception cref="ArgumentNullException"><paramref name="docIdSets"/> is <c>null</c>.</exception>
         public static WAH8DocIdSet Union(ICollection<WAH8DocIdSet> docIdSets, int indexInterval)
         {
+            // LUCENENET: Added guard clause for null
+            if (docIdSets is null)
+                throw new ArgumentNullException(nameof(docIdSets));
+
             switch (docIdSets.Count)
             {
                 case 0:
@@ -246,14 +253,22 @@ namespace Lucene.Net.Util
 
         private sealed class PriorityQueueAnonymousClass : PriorityQueue<WAH8DocIdSet.Iterator>
         {
+#nullable enable
             public PriorityQueueAnonymousClass(int numSets)
                 : base(numSets)
             {
             }
+#nullable restore
 
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
             protected internal override bool LessThan(Iterator a, Iterator b)
             {
+                // LUCENENET specific - added guard clauses
+                if (a is null)
+                    throw new ArgumentException(nameof(a));
+                if (b is null)
+                    throw new ArgumentException(nameof(b));
+
                 return a.wordNum < b.wordNum;
             }
         }