File: src\libraries\System.Private.CoreLib\src\System\Runtime\CompilerServices\ConditionalWeakTable.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Threading;
 
namespace System.Runtime.CompilerServices
{
    public sealed class ConditionalWeakTable<TKey, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
        where TKey : class
        where TValue : class?
    {
        // Lifetimes of keys and values:
        // Inserting a key and value into the dictionary will not
        // prevent the key from dying, even if the key is strongly reachable
        // from the value. Once the key dies, the dictionary automatically removes
        // the key/value entry.
        //
        // Thread safety guarantees:
        // ConditionalWeakTable is fully thread-safe and requires no
        // additional locking to be done by callers.
        //
        // OOM guarantees:
        // Will not corrupt unmanaged handle table on OOM. No guarantees
        // about managed weak table consistency. Native handles reclamation
        // may be delayed until appdomain shutdown.
 
        private const int InitialCapacity = 8;  // Initial length of the table. Must be a power of two.
        private readonly object _lock;          // This lock protects all mutation of data in the table.  Readers do not take this lock.
        private volatile Container _container;  // The actual storage for the table; swapped out as the table grows.
        private int _activeEnumeratorRefCount;  // The number of outstanding enumerators on the table
 
        public ConditionalWeakTable()
        {
            _lock = new object();
            _container = new Container(this);
        }
 
        /// <summary>Gets the value of the specified key.</summary>
        /// <param name="key">key of the value to find. Cannot be null.</param>
        /// <param name="value">
        /// If the key is found, contains the value associated with the key upon method return.
        /// If the key is not found, contains default(TValue).
        /// </param>
        /// <returns>Returns "true" if key was found, "false" otherwise.</returns>
        /// <remarks>
        /// The key may get garbage collected during the TryGetValue operation. If so, TryGetValue
        /// may at its discretion, return "false" and set "value" to the default (as if the key was not present.)
        /// </remarks>
        public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
        {
            if (key is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
 
            return _container.TryGetValueWorker(key, out value);
        }
 
        /// <summary>Adds a key to the table.</summary>
        /// <param name="key">key to add. May not be null.</param>
        /// <param name="value">value to associate with key.</param>
        /// <remarks>
        /// If the key is already entered into the dictionary, this method throws an exception.
        /// The key may get garbage collected during the Add() operation. If so, Add()
        /// has the right to consider any prior entries successfully removed and add a new entry without
        /// throwing an exception.
        /// </remarks>
        public void Add(TKey key, TValue value)
        {
            if (key is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
 
            lock (_lock)
            {
                int entryIndex = _container.FindEntry(key, out _);
                if (entryIndex != -1)
                {
                    ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
                }
 
                CreateEntry(key, value);
            }
        }
 
        /// <summary>Adds a key to the table if it doesn't already exist.</summary>
        /// <param name="key">The key to add.</param>
        /// <param name="value">The key's property value.</param>
        /// <returns>true if the key/value pair was added; false if the table already contained the key.</returns>
        public bool TryAdd(TKey key, TValue value)
        {
            if (key is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
 
            lock (_lock)
            {
                int entryIndex = _container.FindEntry(key, out _);
                if (entryIndex != -1)
                {
                    return false;
                }
 
                CreateEntry(key, value);
                return true;
            }
        }
 
        /// <summary>Adds the key and value if the key doesn't exist, or updates the existing key's value if it does exist.</summary>
        /// <param name="key">key to add or update. May not be null.</param>
        /// <param name="value">value to associate with key.</param>
        public void AddOrUpdate(TKey key, TValue value)
        {
            if (key is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
 
            lock (_lock)
            {
                int entryIndex = _container.FindEntry(key, out _);
 
                // if we found a key we should just update, if no we should create a new entry.
                if (entryIndex != -1)
                {
                    _container.UpdateValue(entryIndex, value);
                }
                else
                {
                    CreateEntry(key, value);
                }
            }
        }
 
        /// <summary>Removes a key and its value from the table.</summary>
        /// <param name="key">key to remove. May not be null.</param>
        /// <returns>true if the key is found and removed. Returns false if the key was not in the dictionary.</returns>
        /// <remarks>
        /// The key may get garbage collected during the Remove() operation. If so,
        /// Remove() will not fail or throw, however, the return value can be either true or false
        /// depending on who wins the race.
        /// </remarks>
        public bool Remove(TKey key)
        {
            if (key is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
 
            lock (_lock)
            {
                return _container.Remove(key);
            }
        }
 
        /// <summary>Clear all the key/value pairs</summary>
        public void Clear()
        {
            lock (_lock)
            {
                // To clear, we would prefer to simply drop the existing container
                // and replace it with an empty one, as that's overall more efficient.
                // However, if there are any active enumerators, we don't want to do
                // that as it will end up removing all of the existing entries and
                // allowing new items to be added at the same indices when the container
                // is filled and replaced, and one of the guarantees we try to make with
                // enumeration is that new items added after enumeration starts won't be
                // included in the enumeration. As such, if there are active enumerators,
                // we simply use the container's removal functionality to remove all of the
                // keys; then when the table is resized, if there are still active enumerators,
                // these empty slots will be maintained.
                if (_activeEnumeratorRefCount > 0)
                {
                    _container.RemoveAllKeys();
                }
                else
                {
                    _container = new Container(this);
                }
            }
        }
 
        /// <summary>
        /// Atomically searches for a specified key in the table and returns the corresponding value.
        /// If the key does not exist in the table, the method invokes a callback method to create a
        /// value that is bound to the specified key.
        /// </summary>
        /// <param name="key">key of the value to find. Cannot be null.</param>
        /// <param name="createValueCallback">callback that creates value for key. Cannot be null.</param>
        /// <returns></returns>
        /// <remarks>
        /// If multiple threads try to initialize the same key, the table may invoke createValueCallback
        /// multiple times with the same key. Exactly one of these calls will succeed and the returned
        /// value of that call will be the one added to the table and returned by all the racing GetValue() calls.
        /// This rule permits the table to invoke createValueCallback outside the internal table lock
        /// to prevent deadlocks.
        /// </remarks>
        public TValue GetValue(TKey key, CreateValueCallback createValueCallback)
        {
            ArgumentNullException.ThrowIfNull(createValueCallback);
 
            // key is validated by TryGetValue
            return TryGetValue(key, out TValue? existingValue) ?
                existingValue :
                GetValueLocked(key, createValueCallback);
        }
 
        private TValue GetValueLocked(TKey key, CreateValueCallback createValueCallback)
        {
            // If we got here, the key was not in the table. Invoke the callback (outside the lock)
            // to generate the new value for the key.
            TValue newValue = createValueCallback(key);
 
            lock (_lock)
            {
                // Now that we've taken the lock, must recheck in case we lost a race to add the key.
                if (_container.TryGetValueWorker(key, out TValue? existingValue))
                {
                    return existingValue;
                }
                else
                {
                    // Verified in-lock that we won the race to add the key. Add it now.
                    CreateEntry(key, newValue);
                    return newValue;
                }
            }
        }
 
        /// <summary>
        /// Helper method to call GetValue without passing a creation delegate.  Uses Activator.CreateInstance
        /// to create new instances as needed.  If TValue does not have a default constructor, this will throw.
        /// </summary>
        /// <param name="key">key of the value to find. Cannot be null.</param>
        public TValue GetOrCreateValue(TKey key) => GetValue(key, _ => Activator.CreateInstance<TValue>());
 
        public delegate TValue CreateValueCallback(TKey key);
 
        /// <summary>Gets an enumerator for the table.</summary>
        /// <remarks>
        /// The returned enumerator will not extend the lifetime of
        /// any object pairs in the table, other than the one that's Current.  It will not return entries
        /// that have already been collected, nor will it return entries added after the enumerator was
        /// retrieved.  It may not return all entries that were present when the enumerat was retrieved,
        /// however, such as not returning entries that were collected or removed after the enumerator
        /// was retrieved but before they were enumerated.
        /// </remarks>
        IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
        {
            lock (_lock)
            {
                Container c = _container;
                return c is null || c.FirstFreeEntry == 0 ?
                    GenericEmptyEnumerator<KeyValuePair<TKey, TValue>>.Instance :
                    new Enumerator(this);
            }
        }
 
        IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<KeyValuePair<TKey, TValue>>)this).GetEnumerator();
 
        /// <summary>Provides an enumerator for the table.</summary>
        private sealed class Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
        {
            // The enumerator would ideally hold a reference to the Container and the end index within that
            // container.  However, the safety of the CWT depends on the only reference to the Container being
            // from the CWT itself; the Container then employs a two-phase finalization scheme, where the first
            // phase nulls out that parent CWT's reference, guaranteeing that the second time it's finalized there
            // can be no other existing references to it in use that would allow for concurrent usage of the
            // native handles with finalization.  We would break that if we allowed this Enumerator to hold a
            // reference to the Container.  Instead, the Enumerator holds a reference to the CWT rather than to
            // the Container, and it maintains the CWT._activeEnumeratorRefCount field to track whether there
            // are outstanding enumerators that have yet to be disposed/finalized.  If there aren't any, the CWT
            // behaves as it normally does.  If there are, certain operations are affected, in particular resizes.
            // Normally when the CWT is resized, it enumerates the contents of the table looking for indices that
            // contain entries which have been collected or removed, and it frees those up, effectively moving
            // down all subsequent entries in the container (not in the existing container, but in a replacement).
            // This, however, would cause the enumerator's understanding of indices to break.  So, as long as
            // there is any outstanding enumerator, no compaction is performed.
 
            private ConditionalWeakTable<TKey, TValue>? _table; // parent table, set to null when disposed
            private readonly int _maxIndexInclusive;            // last index in the container that should be enumerated
            private int _currentIndex;                          // the current index into the container
            private KeyValuePair<TKey, TValue> _current;        // the current entry set by MoveNext and returned from Current
 
            public Enumerator(ConditionalWeakTable<TKey, TValue> table)
            {
                Debug.Assert(table != null, "Must provide a valid table");
                Debug.Assert(Monitor.IsEntered(table._lock), "Must hold the _lock lock to construct the enumerator");
                Debug.Assert(table._container != null, "Should not be used on a finalized table");
                Debug.Assert(table._container.FirstFreeEntry > 0, "Should have returned an empty enumerator instead");
 
                // Store a reference to the parent table and increase its active enumerator count.
                _table = table;
                Debug.Assert(table._activeEnumeratorRefCount >= 0, "Should never have a negative ref count before incrementing");
                table._activeEnumeratorRefCount++;
 
                // Store the max index to be enumerated.
                _maxIndexInclusive = table._container.FirstFreeEntry - 1;
                _currentIndex = -1;
            }
 
            ~Enumerator()
            {
                Dispose();
            }
 
            public void Dispose()
            {
                // Use an interlocked operation to ensure that only one thread can get access to
                // the _table for disposal and thus only decrement the ref count once.
                ConditionalWeakTable<TKey, TValue>? table = Interlocked.Exchange(ref _table, null);
                if (table != null)
                {
                    // Ensure we don't keep the last current alive unnecessarily
                    _current = default;
 
                    // Decrement the ref count that was incremented when constructed
                    lock (table._lock)
                    {
                        table._activeEnumeratorRefCount--;
                        Debug.Assert(table._activeEnumeratorRefCount >= 0, "Should never have a negative ref count after decrementing");
                    }
 
                    // Finalization is purely to decrement the ref count.  We can suppress it now.
                    GC.SuppressFinalize(this);
                }
            }
 
            public bool MoveNext()
            {
                // Start by getting the current table.  If it's already been disposed, it will be null.
                ConditionalWeakTable<TKey, TValue>? table = _table;
                if (table != null)
                {
                    // Once have the table, we need to lock to synchronize with other operations on
                    // the table, like adding.
                    lock (table._lock)
                    {
                        // From the table, we have to get the current container.  This could have changed
                        // since we grabbed the enumerator, but the index-to-pair mapping should not have
                        // due to there being at least one active enumerator.  If the table (or rather its
                        // container at the time) has already been finalized, this will be null.
                        Container c = table._container;
                        if (c != null)
                        {
                            // We have the container.  Find the next entry to return, if there is one.
                            // We need to loop as we may try to get an entry that's already been removed
                            // or collected, in which case we try again.
                            while (_currentIndex < _maxIndexInclusive)
                            {
                                _currentIndex++;
                                if (c.TryGetEntry(_currentIndex, out TKey? key, out TValue? value))
                                {
                                    _current = new KeyValuePair<TKey, TValue>(key, value);
                                    return true;
                                }
                            }
                        }
                    }
                }
 
                // Nothing more to enumerate.
                return false;
            }
 
            public KeyValuePair<TKey, TValue> Current
            {
                get
                {
                    if (_currentIndex < 0)
                    {
                        ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
                    }
                    return _current;
                }
            }
 
            object? IEnumerator.Current => Current;
 
            public void Reset() { }
        }
 
        /// <summary>Worker for adding a new key/value pair. Will resize the container if it is full.</summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        private void CreateEntry(TKey key, TValue value)
        {
            Debug.Assert(Monitor.IsEntered(_lock));
            Debug.Assert(key != null); // key already validated as non-null and not already in table.
 
            Container c = _container;
            if (!c.HasCapacity)
            {
                _container = c = c.Resize();
            }
            c.CreateEntryNoResize(key, value);
        }
 
        //--------------------------------------------------------------------------------------------
        // Entry can be in one of four states:
        //
        //    - Unused (stored with an index _firstFreeEntry and above)
        //         depHnd.IsAllocated == false
        //         hashCode == <dontcare>
        //         next == <dontcare>)
        //
        //    - Used with live key (linked into a bucket list where _buckets[hashCode & (_buckets.Length - 1)] points to first entry)
        //         depHnd.IsAllocated == true, depHnd.GetPrimary() != null
        //         hashCode == RuntimeHelpers.GetHashCode(depHnd.GetPrimary()) & int.MaxValue
        //         next links to next Entry in bucket.
        //
        //    - Used with dead key (linked into a bucket list where _buckets[hashCode & (_buckets.Length - 1)] points to first entry)
        //         depHnd.IsAllocated == true, depHnd.GetPrimary() is null
        //         hashCode == <notcare>
        //         next links to next Entry in bucket.
        //
        //    - Has been removed from the table (by a call to Remove)
        //         depHnd.IsAllocated == true, depHnd.GetPrimary() == <notcare>
        //         hashCode == -1
        //         next links to next Entry in bucket.
        //
        // The only difference between "used with live key" and "used with dead key" is that
        // depHnd.GetPrimary() returns null. The transition from "used with live key" to "used with dead key"
        // happens asynchronously as a result of normal garbage collection. The dictionary itself
        // receives no notification when this happens.
        //
        // When the dictionary grows the _entries table, it scours it for expired keys and does not
        // add those to the new container.
        //--------------------------------------------------------------------------------------------
        private struct Entry
        {
            public DependentHandle depHnd;      // Holds key and value using a weak reference for the key and a strong reference
                                                // for the value that is traversed only if the key is reachable without going through the value.
            public int HashCode;    // Cached copy of key's hashcode
            public int Next;        // Index of next entry, -1 if last
        }
 
        /// <summary>
        /// Container holds the actual data for the table.  A given instance of Container always has the same capacity.  When we need
        /// more capacity, we create a new Container, copy the old one into the new one, and discard the old one.  This helps enable lock-free
        /// reads from the table, as readers never need to deal with motion of entries due to rehashing.
        /// </summary>
        private sealed class Container
        {
            private readonly ConditionalWeakTable<TKey, TValue> _parent;  // the ConditionalWeakTable with which this container is associated
            private int[] _buckets;                // _buckets[hashcode & (_buckets.Length - 1)] contains index of the first entry in bucket (-1 if empty)
            private Entry[] _entries;              // the table entries containing the stored dependency handles
            private int _firstFreeEntry;           // _firstFreeEntry < _entries.Length => table has capacity,  entries grow from the bottom of the table.
            private bool _invalid;                 // flag detects if OOM or other background exception threw us out of the lock.
            private bool _finalized;               // set to true when initially finalized
            private volatile object? _oldKeepAlive; // used to ensure the next allocated container isn't finalized until this one is GC'd
 
            internal Container(ConditionalWeakTable<TKey, TValue> parent)
            {
                Debug.Assert(parent != null);
                Debug.Assert(BitOperations.IsPow2(InitialCapacity));
 
                const int Size = InitialCapacity;
                _buckets = new int[Size];
                for (int i = 0; i < _buckets.Length; i++)
                {
                    _buckets[i] = -1;
                }
                _entries = new Entry[Size];
 
                // Only store the parent after all of the allocations have happened successfully.
                // Otherwise, as part of growing or clearing the container, we could end up allocating
                // a new Container that fails (OOMs) part way through construction but that gets finalized
                // and ends up clearing out some other container present in the associated CWT.
                _parent = parent;
            }
 
            private Container(ConditionalWeakTable<TKey, TValue> parent, int[] buckets, Entry[] entries, int firstFreeEntry)
            {
                Debug.Assert(parent != null);
                Debug.Assert(buckets != null);
                Debug.Assert(entries != null);
                Debug.Assert(buckets.Length == entries.Length);
                Debug.Assert(BitOperations.IsPow2(buckets.Length));
 
                _parent = parent;
                _buckets = buckets;
                _entries = entries;
                _firstFreeEntry = firstFreeEntry;
            }
 
            internal bool HasCapacity => _firstFreeEntry < _entries.Length;
 
            internal int FirstFreeEntry => _firstFreeEntry;
 
            /// <summary>Worker for adding a new key/value pair. Container must NOT be full.</summary>
            internal void CreateEntryNoResize(TKey key, TValue value)
            {
                Debug.Assert(key != null); // key already validated as non-null and not already in table.
                Debug.Assert(HasCapacity);
 
                VerifyIntegrity();
                _invalid = true;
 
                int hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue;
                int newEntry = _firstFreeEntry++;
 
                _entries[newEntry].HashCode = hashCode;
                _entries[newEntry].depHnd = new DependentHandle(key, value);
                int bucket = hashCode & (_buckets.Length - 1);
                _entries[newEntry].Next = _buckets[bucket];
 
                // This write must be volatile, as we may be racing with concurrent readers.  If they see
                // the new entry, they must also see all of the writes earlier in this method.
                Volatile.Write(ref _buckets[bucket], newEntry);
 
                _invalid = false;
            }
 
            /// <summary>Worker for finding a key/value pair. Must hold _lock.</summary>
            internal bool TryGetValueWorker(TKey key, [MaybeNullWhen(false)] out TValue value)
            {
                Debug.Assert(key != null); // Key already validated as non-null
 
                int entryIndex = FindEntry(key, out object? secondary);
                value = Unsafe.As<TValue>(secondary);
                return entryIndex != -1;
            }
 
            /// <summary>
            /// Returns -1 if not found (if key expires during FindEntry, this can be treated as "not found.").
            /// Must hold _lock, or be prepared to retry the search while holding _lock.
            /// </summary>
            /// <remarks>This method requires <paramref name="value"/> to be on the stack to be properly tracked.</remarks>
            internal int FindEntry(TKey key, out object? value)
            {
                Debug.Assert(key != null); // Key already validated as non-null.
 
                int hashCode = RuntimeHelpers.TryGetHashCode(key);
 
                if (hashCode == 0)
                {
                    // No hash code has been assigned to the key, so therefore it has not been added
                    // to any ConditionalWeakTable.
                    value = null;
                    return -1;
                }
 
                hashCode &= int.MaxValue;
                int bucket = hashCode & (_buckets.Length - 1);
                for (int entriesIndex = Volatile.Read(ref _buckets[bucket]); entriesIndex != -1; entriesIndex = _entries[entriesIndex].Next)
                {
                    if (_entries[entriesIndex].HashCode == hashCode && _entries[entriesIndex].depHnd.UnsafeGetTargetAndDependent(out value) == key)
                    {
                        GC.KeepAlive(this); // Ensure we don't get finalized while accessing DependentHandle
 
                        return entriesIndex;
                    }
                }
 
                GC.KeepAlive(this); // Ensure we don't get finalized while accessing DependentHandle
                value = null;
                return -1;
            }
 
            /// <summary>Gets the entry at the specified entry index.</summary>
            internal bool TryGetEntry(int index, [NotNullWhen(true)] out TKey? key, [MaybeNullWhen(false)] out TValue value)
            {
                if (index < _entries.Length)
                {
                    object? oKey = _entries[index].depHnd.UnsafeGetTargetAndDependent(out object? oValue);
 
                    GC.KeepAlive(this); // Ensure we don't get finalized while accessing DependentHandle
 
                    if (oKey != null)
                    {
                        key = Unsafe.As<TKey>(oKey);
                        value = Unsafe.As<TValue>(oValue);
                        return true;
                    }
                }
 
                key = default;
                value = default;
                return false;
            }
 
            /// <summary>Removes all of the keys in the table.</summary>
            internal void RemoveAllKeys()
            {
                for (int i = 0; i < _firstFreeEntry; i++)
                {
                    RemoveIndex(i);
                }
            }
 
            /// <summary>Removes the specified key from the table, if it exists.</summary>
            internal bool Remove(TKey key)
            {
                VerifyIntegrity();
 
                int entryIndex = FindEntry(key, out _);
                if (entryIndex != -1)
                {
                    RemoveIndex(entryIndex);
                    return true;
                }
 
                return false;
            }
 
            private void RemoveIndex(int entryIndex)
            {
                Debug.Assert(entryIndex >= 0 && entryIndex < _firstFreeEntry);
 
                ref Entry entry = ref _entries[entryIndex];
 
                // We do not free the handle here, as we may be racing with readers who already saw the hash code.
                // Instead, we simply overwrite the entry's hash code, so subsequent reads will ignore it.
                // The handle will be free'd in Container's finalizer, after the table is resized or discarded.
                Volatile.Write(ref entry.HashCode, -1);
 
                // Also, clear the key to allow GC to collect objects pointed to by the entry
                entry.depHnd.UnsafeSetTargetToNull();
            }
 
            internal void UpdateValue(int entryIndex, TValue newValue)
            {
                Debug.Assert(entryIndex != -1);
 
                VerifyIntegrity();
                _invalid = true;
 
                _entries[entryIndex].depHnd.UnsafeSetDependent(newValue);
 
                _invalid = false;
            }
 
            /// <summary>Resize, and scrub expired keys off bucket lists. Must hold _lock.</summary>
            /// <remarks>
            /// _firstEntry is less than _entries.Length on exit, that is, the table has at least one free entry.
            /// </remarks>
            internal Container Resize()
            {
                Debug.Assert(!HasCapacity);
 
                bool hasExpiredEntries = false;
                int newSize = _buckets.Length;
 
                if (_parent is null || _parent._activeEnumeratorRefCount == 0)
                {
                    // If any expired or removed keys exist, we won't resize.
                    // If there any active enumerators, though, we don't want
                    // to compact and thus have no expired entries.
                    for (int entriesIndex = 0; entriesIndex < _entries.Length; entriesIndex++)
                    {
                        ref Entry entry = ref _entries[entriesIndex];
 
                        if (entry.HashCode == -1)
                        {
                            // the entry was removed
                            hasExpiredEntries = true;
                            break;
                        }
 
                        if (entry.depHnd.IsAllocated && entry.depHnd.UnsafeGetTarget() is null)
                        {
                            // the entry has expired
                            hasExpiredEntries = true;
                            break;
                        }
                    }
                }
 
                if (!hasExpiredEntries)
                {
                    // Not necessary to check for overflow here, the attempt to allocate new arrays will throw
                    newSize = _buckets.Length * 2;
                }
 
                return Resize(newSize);
            }
 
            internal Container Resize(int newSize)
            {
                Debug.Assert(newSize >= _buckets.Length);
                Debug.Assert(BitOperations.IsPow2(newSize));
 
                // Reallocate both buckets and entries and rebuild the bucket and entries from scratch.
                // This serves both to scrub entries with expired keys and to put the new entries in the proper bucket.
                int[] newBuckets = new int[newSize];
                for (int bucketIndex = 0; bucketIndex < newBuckets.Length; bucketIndex++)
                {
                    newBuckets[bucketIndex] = -1;
                }
                Entry[] newEntries = new Entry[newSize];
                int newEntriesIndex = 0;
                bool activeEnumerators = _parent != null && _parent._activeEnumeratorRefCount > 0;
                bool transferredHandles;
 
                // Migrate existing entries to the new table.
                if (activeEnumerators)
                {
                    transferredHandles = true;
 
                    // There's at least one active enumerator, which means we don't want to
                    // remove any expired/removed entries, in order to not affect existing
                    // entries indices.  Copy over the entries while rebuilding the buckets list,
                    // as the buckets are dependent on the buckets list length, which is changing.
                    for (; newEntriesIndex < _entries.Length; newEntriesIndex++)
                    {
                        ref Entry oldEntry = ref _entries[newEntriesIndex];
                        ref Entry newEntry = ref newEntries[newEntriesIndex];
                        int hashCode = oldEntry.HashCode;
 
                        newEntry.HashCode = hashCode;
                        newEntry.depHnd = oldEntry.depHnd;
                        int bucket = hashCode & (newBuckets.Length - 1);
                        newEntry.Next = newBuckets[bucket];
                        newBuckets[bucket] = newEntriesIndex;
                    }
                }
                else
                {
                    transferredHandles = false;
 
                    // There are no active enumerators, which means we want to compact by
                    // removing expired/removed entries.
                    for (int entriesIndex = 0; entriesIndex < _entries.Length; entriesIndex++)
                    {
                        ref Entry oldEntry = ref _entries[entriesIndex];
                        int hashCode = oldEntry.HashCode;
                        DependentHandle depHnd = oldEntry.depHnd;
                        if (hashCode != -1 && depHnd.IsAllocated)
                        {
                            if (depHnd.UnsafeGetTarget() is not null)
                            {
                                transferredHandles = true;
 
                                ref Entry newEntry = ref newEntries[newEntriesIndex];
 
                                // Entry is used and has not expired. Link it into the appropriate bucket list.
                                newEntry.HashCode = hashCode;
                                newEntry.depHnd = depHnd;
                                int bucket = hashCode & (newBuckets.Length - 1);
                                newEntry.Next = newBuckets[bucket];
                                newBuckets[bucket] = newEntriesIndex;
                                newEntriesIndex++;
                            }
                            else
                            {
                                // Pretend the item was removed, so that this container's finalizer
                                // will clean up this dependent handle.
                                Volatile.Write(ref oldEntry.HashCode, -1);
                            }
                        }
                    }
                }
 
                // Create the new container.  We want to transfer the responsibility of freeing the handles from
                // the old container to the new container, and also ensure that the new container isn't finalized
                // while the old container may still be in use.  As such, we store a reference from the old container
                // to the new one, which will keep the new container alive as long as the old one is.
                var newContainer = new Container(_parent!, newBuckets, newEntries, newEntriesIndex);
                if (activeEnumerators)
                {
                    // If there are active enumerators, both the old container and the new container may be storing
                    // the same entries with -1 hash codes, which the finalizer will clean up even if the container
                    // is not the active container for the table.  To prevent that, we want to stop the old container
                    // from being finalized, as it no longer has any responsibility for any cleanup.
                    GC.SuppressFinalize(this);
                }
 
                if (transferredHandles)
                {
                    // Once this is set, the old container's finalizer will not free transferred dependent handles,
                    // and the new container's finalizer can't be run until this container is no longer in use.
                    _oldKeepAlive = newContainer;
                }
 
                GC.KeepAlive(this); // ensure we don't get finalized while accessing DependentHandles.
 
                return newContainer;
            }
 
            private void VerifyIntegrity()
            {
                if (_invalid)
                {
                    throw new InvalidOperationException(SR.InvalidOperation_CollectionCorrupted);
                }
            }
 
            ~Container()
            {
                // Skip doing anything if the container is invalid, including if somehow
                // the container object was allocated but its associated table never set.
                if (_invalid || _parent is null)
                {
                    return;
                }
 
                // It's possible that the ConditionalWeakTable could have been resurrected, in which case code could
                // be accessing this Container as it's being finalized.  We don't support usage after finalization,
                // but we also don't want to potentially corrupt state by allowing dependency handles to be used as
                // or after they've been freed.  To avoid that, if it's at all possible that another thread has a
                // reference to this container via the CWT, we remove such a reference and then re-register for
                // finalization: the next time around, we can be sure that no references remain to this and we can
                // clean up the dependency handles without fear of corruption.
                if (!_finalized)
                {
                    _finalized = true;
                    lock (_parent._lock)
                    {
                        if (_parent._container == this)
                        {
                            _parent._container = null!;
                        }
                    }
                    GC.ReRegisterForFinalize(this); // next time it's finalized, we'll be sure there are no remaining refs
                    return;
                }
 
                Entry[] entries = _entries;
                _invalid = true;
                _entries = null!;
                _buckets = null!;
 
                if (entries != null)
                {
                    for (int entriesIndex = 0; entriesIndex < entries.Length; entriesIndex++)
                    {
                        // We need to free handles in two cases:
                        // - If this container still owns the dependency handle (meaning ownership hasn't been transferred
                        //   to another container that replaced this one), then it should be freed.
                        // - If this container had the entry removed, then even if in general ownership was transferred to
                        //   another container, removed entries are not, therefore this container must free them.
                        if (_oldKeepAlive is null || entries[entriesIndex].HashCode == -1)
                        {
                            entries[entriesIndex].depHnd.Dispose();
                        }
                    }
                }
            }
        }
    }
}