}
else
{
- List<RuntimeType?> al = new List<RuntimeType?>();
+ var al = new HashSet<RuntimeType>();
// Get all constraints
Type[] constraints = declaringType.GetGenericParameterConstraints();
Type[] temp = constraint.GetInterfaces();
for (int j = 0; j < temp.Length; j++)
- al.Add(temp[j] as RuntimeType);
+ al.Add((RuntimeType)temp[j]);
}
- // Remove duplicates
- Dictionary<RuntimeType, RuntimeType> ht = new Dictionary<RuntimeType, RuntimeType>();
- for (int i = 0; i < al.Count; i++)
+ // Populate list, without duplicates
+ foreach (RuntimeType rt in al)
{
- RuntimeType constraint = al[i]!;
- if (!ht.ContainsKey(constraint))
- ht[constraint] = constraint;
- }
-
- RuntimeType[] interfaces = new RuntimeType[ht.Values.Count];
- ht.Values.CopyTo(interfaces, 0);
-
- // Populate link-list
- for (int i = 0; i < interfaces.Length; i++)
- {
- if (filter.RequiresStringComparison())
+ if (!filter.RequiresStringComparison() || filter.Match(RuntimeTypeHandle.GetUtf8Name(rt)))
{
- if (!filter.Match(RuntimeTypeHandle.GetUtf8Name(interfaces[i])))
- continue;
+ list.Add(rt);
}
-
- list.Add(interfaces[i]);
}
}
<data name="Arg_BitArrayTypeUnsupported" xml:space="preserve">
<value>Only supported array types for CopyTo on BitArrays are Boolean[], Int32[] and Byte[].</value>
</data>
- <data name="Arg_HSCapacityOverflow" xml:space="preserve">
- <value>HashSet capacity is too big.</value>
- </data>
<data name="Arg_HTCapacityOverflow" xml:space="preserve">
<value>Hashtable's capacity overflowed and went negative. Check load factor, capacity and the current size of the table.</value>
</data>
</ItemGroup>
<ItemGroup>
<Compile Include="System\Collections\BitArray.cs" />
- <Compile Include="System\Collections\Generic\BitHelper.cs" />
<Compile Include="System\Collections\Generic\CollectionExtensions.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\ICollectionDebugView.cs"
Link="Common\System\Collections\Generic\ICollectionDebugView.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\IDictionaryDebugView.cs"
Link="Common\System\Collections\Generic\IDictionaryDebugView.cs" />
- <Compile Include="System\Collections\Generic\HashSet.cs" />
- <Compile Include="System\Collections\Generic\HashSetEqualityComparer.cs" />
<Compile Include="System\Collections\Generic\LinkedList.cs" />
<Compile Include="System\Collections\Generic\Queue.cs" />
<Compile Include="System\Collections\Generic\QueueDebugView.cs" />
<!-- Shared Common -->
<Compile Include="$(CoreLibSharedDir)System\Collections\HashHelpers.cs"
Link="Common\System\Collections\HashHelpers.cs" />
+ <Compile Include="$(CommonPath)System\Collections\Generic\BitHelper.cs"
+ Link="Common\System\Collections\Generic\BitHelper.cs" />
<Compile Include="$(CommonPath)System\Collections\Generic\EnumerableHelpers.cs"
Link="Common\System\Collections\Generic\EnumerableHelpers.cs" />
</ItemGroup>
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-using System.Runtime.Serialization;
-
-namespace System.Collections.Generic
-{
- /// <summary>
- /// Implementation notes:
- /// This uses an array-based implementation similar to <see cref="Dictionary{TKey, TValue}"/>, using a buckets array
- /// to map hash values to the Slots array. Items in the Slots array that hash to the same value
- /// are chained together through the "next" indices.
- ///
- /// The capacity is always prime; so during resizing, the capacity is chosen as the next prime
- /// greater than double the last capacity.
- ///
- /// The underlying data structures are lazily initialized. Because of the observation that,
- /// in practice, hashtables tend to contain only a few elements, the initial capacity is
- /// set very small (3 elements) unless the ctor with a collection is used.
- ///
- /// The +/- 1 modifications in methods that add, check for containment, etc allow us to
- /// distinguish a hash code of 0 from an uninitialized bucket. This saves us from having to
- /// reset each bucket to -1 when resizing. See Contains, for example.
- ///
- /// Set methods such as UnionWith, IntersectWith, ExceptWith, and SymmetricExceptWith modify
- /// this set.
- ///
- /// Some operations can perform faster if we can assume "other" contains unique elements
- /// according to this equality comparer. The only times this is efficient to check is if
- /// other is a hashset. Note that checking that it's a hashset alone doesn't suffice; we
- /// also have to check that the hashset is using the same equality comparer. If other
- /// has a different equality comparer, it will have unique elements according to its own
- /// equality comparer, but not necessarily according to ours. Therefore, to go these
- /// optimized routes we check that other is a hashset using the same equality comparer.
- ///
- /// A HashSet with no elements has the properties of the empty set. (See IsSubset, etc. for
- /// special empty set checks.)
- ///
- /// A couple of methods have a special case if other is this (e.g. SymmetricExceptWith).
- /// If we didn't have these checks, we could be iterating over the set and modifying at
- /// the same time.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- [DebuggerTypeProxy(typeof(ICollectionDebugView<>))]
- [DebuggerDisplay("Count = {Count}")]
- [Serializable]
- [System.Runtime.CompilerServices.TypeForwardedFrom("System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
- public class HashSet<T> : ICollection<T>, ISet<T>, IReadOnlyCollection<T>, IReadOnlySet<T>, ISerializable, IDeserializationCallback
- {
- // store lower 31 bits of hash code
- private const int Lower31BitMask = 0x7FFFFFFF;
- // cutoff point, above which we won't do stackallocs. This corresponds to 100 integers.
- private const int StackAllocThreshold = 100;
- // when constructing a hashset from an existing collection, it may contain duplicates,
- // so this is used as the max acceptable excess ratio of capacity to count. Note that
- // this is only used on the ctor and not to automatically shrink if the hashset has, e.g,
- // a lot of adds followed by removes. Users must explicitly shrink by calling TrimExcess.
- // This is set to 3 because capacity is acceptable as 2x rounded up to nearest prime.
- private const int ShrinkThreshold = 3;
-
- // constants for serialization
- private const string CapacityName = "Capacity"; // Do not rename (binary serialization)
- private const string ElementsName = "Elements"; // Do not rename (binary serialization)
- private const string ComparerName = "Comparer"; // Do not rename (binary serialization)
- private const string VersionName = "Version"; // Do not rename (binary serialization)
-
- private int[]? _buckets;
- private Slot[] _slots = default!; // TODO-NULLABLE: This should be Slot[]?, but the resulting annotations causes GenPartialFacadeSource to blow up: error : Unable to cast object of type 'Microsoft.CodeAnalysis.CSharp.Syntax.CompilationUnitSyntax' to type 'Microsoft.CodeAnalysis.CSharp.Syntax.BaseTypeDeclarationSyntax'
- private int _count;
- private int _lastIndex;
- private int _freeList;
- private IEqualityComparer<T>? _comparer;
- private int _version;
-
- private SerializationInfo? _siInfo; // temporary variable needed during deserialization
-
- #region Constructors
-
- public HashSet()
- : this((IEqualityComparer<T>?)null)
- { }
-
- public HashSet(IEqualityComparer<T>? comparer)
- {
- if (comparer == EqualityComparer<T>.Default)
- {
- comparer = null;
- }
-
- _comparer = comparer;
- _lastIndex = 0;
- _count = 0;
- _freeList = -1;
- _version = 0;
- }
-
- public HashSet(int capacity)
- : this(capacity, null)
- { }
-
- public HashSet(IEnumerable<T> collection)
- : this(collection, null)
- { }
-
- /// <summary>
- /// Implementation Notes:
- /// Since resizes are relatively expensive (require rehashing), this attempts to minimize
- /// the need to resize by setting the initial capacity based on size of collection.
- /// </summary>
- /// <param name="collection"></param>
- /// <param name="comparer"></param>
- public HashSet(IEnumerable<T> collection, IEqualityComparer<T>? comparer)
- : this(comparer)
- {
- if (collection == null)
- {
- throw new ArgumentNullException(nameof(collection));
- }
-
- var otherAsHashSet = collection as HashSet<T>;
- if (otherAsHashSet != null && AreEqualityComparersEqual(this, otherAsHashSet))
- {
- CopyFrom(otherAsHashSet);
- }
- else
- {
- // to avoid excess resizes, first set size based on collection's count. Collection
- // may contain duplicates, so call TrimExcess if resulting hashset is larger than
- // threshold
- ICollection<T>? coll = collection as ICollection<T>;
- int suggestedCapacity = coll == null ? 0 : coll.Count;
- Initialize(suggestedCapacity);
-
- UnionWith(collection);
-
- if (_count > 0 && _slots.Length / _count > ShrinkThreshold)
- {
- TrimExcess();
- }
- }
- }
-
- protected HashSet(SerializationInfo info, StreamingContext context)
- {
- // We can't do anything with the keys and values until the entire graph has been
- // deserialized and we have a reasonable estimate that GetHashCode is not going to
- // fail. For the time being, we'll just cache this. The graph is not valid until
- // OnDeserialization has been called.
- _siInfo = info;
- }
-
- // Initializes the HashSet from another HashSet with the same element type and
- // equality comparer.
- private void CopyFrom(HashSet<T> source)
- {
- int count = source._count;
- if (count == 0)
- {
- // As well as short-circuiting on the rest of the work done,
- // this avoids errors from trying to access otherAsHashSet._buckets
- // or otherAsHashSet._slots when they aren't initialized.
- return;
- }
-
- int capacity = source._buckets!.Length;
- int threshold = HashHelpers.ExpandPrime(count + 1);
-
- if (threshold >= capacity)
- {
- _buckets = (int[])source._buckets.Clone();
- _slots = (Slot[])source._slots.Clone();
-
- _lastIndex = source._lastIndex;
- _freeList = source._freeList;
- }
- else
- {
- int lastIndex = source._lastIndex;
- Slot[] slots = source._slots;
- Initialize(count);
- int index = 0;
- for (int i = 0; i < lastIndex; ++i)
- {
- int hashCode = slots[i].hashCode;
- if (hashCode >= 0)
- {
- AddValue(index, hashCode, slots[i].value);
- ++index;
- }
- }
- Debug.Assert(index == count);
- _lastIndex = index;
- }
- _count = count;
- }
-
- public HashSet(int capacity, IEqualityComparer<T>? comparer)
- : this(comparer)
- {
- if (capacity < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(capacity));
- }
-
- if (capacity > 0)
- {
- Initialize(capacity);
- }
- }
-
- #endregion
-
- #region ICollection<T> methods
-
- /// <summary>
- /// Add item to this hashset. This is the explicit implementation of the <see cref="ICollection{T}"/>
- /// interface. The other Add method returns bool indicating whether item was added.
- /// </summary>
- /// <param name="item">item to add</param>
- void ICollection<T>.Add(T item)
- {
- AddIfNotPresent(item);
- }
-
- /// <summary>
- /// Remove all items from this set. This clears the elements but not the underlying
- /// buckets and slots array. Follow this call by TrimExcess to release these.
- /// </summary>
- public void Clear()
- {
- if (_lastIndex > 0)
- {
- Debug.Assert(_buckets != null, "_buckets was null but _lastIndex > 0");
-
- // clear the elements so that the gc can reclaim the references.
- // clear only up to _lastIndex for _slots
- Array.Clear(_slots, 0, _lastIndex);
- Array.Clear(_buckets, 0, _buckets.Length);
- _lastIndex = 0;
- _count = 0;
- _freeList = -1;
- }
- _version++;
- }
-
- /// <summary>
- /// Checks if this hashset contains the item
- /// </summary>
- /// <param name="item">item to check for containment</param>
- /// <returns>true if item contained; false if not</returns>
- public bool Contains(T item)
- {
- int[]? buckets = _buckets;
-
- if (buckets != null)
- {
- int collisionCount = 0;
- Slot[] slots = _slots;
- IEqualityComparer<T>? comparer = _comparer;
-
- if (comparer == null)
- {
- int hashCode = item == null ? 0 : InternalGetHashCode(item.GetHashCode());
-
- if (typeof(T).IsValueType)
- {
- // see note at "HashSet" level describing why "- 1" appears in for loop
- for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && EqualityComparer<T>.Default.Equals(slots[i].value, item))
- {
- return true;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- else
- {
- // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize
- // https://github.com/dotnet/runtime/issues/10050
- // So cache in a local rather than get EqualityComparer per loop iteration
- EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
-
- // see note at "HashSet" level describing why "- 1" appears in for loop
- for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && defaultComparer.Equals(slots[i].value, item))
- {
- return true;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- }
- else
- {
- int hashCode = item == null ? 0 : InternalGetHashCode(comparer.GetHashCode(item));
-
- // see note at "HashSet" level describing why "- 1" appears in for loop
- for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, item))
- {
- return true;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- }
-
- // either _buckets is null or wasn't found
- return false;
- }
-
- /// <summary>
- /// Copy items in this hashset to array, starting at arrayIndex
- /// </summary>
- /// <param name="array">array to add items to</param>
- /// <param name="arrayIndex">index to start at</param>
- public void CopyTo(T[] array, int arrayIndex)
- {
- CopyTo(array, arrayIndex, _count);
- }
-
- /// <summary>
- /// Remove item from this hashset
- /// </summary>
- /// <param name="item">item to remove</param>
- /// <returns>true if removed; false if not (i.e. if the item wasn't in the HashSet)</returns>
- public bool Remove(T item)
- {
- int hashCode;
- int bucket;
- int last = -1;
- int collisionCount = 0;
- int i;
- Slot[] slots;
- IEqualityComparer<T>? comparer = _comparer;
-
- if (_buckets != null)
- {
- slots = _slots;
-
- if (comparer == null)
- {
- hashCode = item == null ? 0 : InternalGetHashCode(item.GetHashCode());
- bucket = hashCode % _buckets!.Length;
-
- if (typeof(T).IsValueType)
- {
- for (i = _buckets[bucket] - 1; i >= 0; last = i, i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && EqualityComparer<T>.Default.Equals(slots[i].value, item))
- {
- goto ReturnFound;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException("SR.InvalidOperation_ConcurrentOperationsNotSupported");
- }
- collisionCount++;
- }
- }
- else
- {
- // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize
- // https://github.com/dotnet/runtime/issues/10050
- // So cache in a local rather than get EqualityComparer per loop iteration
- EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
-
- for (i = _buckets[bucket] - 1; i >= 0; last = i, i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && defaultComparer.Equals(slots[i].value, item))
- {
- goto ReturnFound;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException("SR.InvalidOperation_ConcurrentOperationsNotSupported");
- }
- collisionCount++;
- }
- }
- }
- else
- {
- hashCode = item == null ? 0 : InternalGetHashCode(comparer.GetHashCode(item));
- bucket = hashCode % _buckets!.Length;
-
- for (i = _buckets[bucket] - 1; i >= 0; last = i, i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, item))
- {
- goto ReturnFound;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException("SR.InvalidOperation_ConcurrentOperationsNotSupported");
- }
- collisionCount++;
- }
- }
- }
- // either _buckets is null or wasn't found
- return false;
-
- ReturnFound:
- if (last < 0)
- {
- // first iteration; update buckets
- _buckets[bucket] = slots[i].next + 1;
- }
- else
- {
- // subsequent iterations; update 'next' pointers
- slots[last].next = slots[i].next;
- }
- slots[i].hashCode = -1;
- if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
- {
- slots[i].value = default!;
- }
- slots[i].next = _freeList;
-
- _count--;
- _version++;
- if (_count == 0)
- {
- _lastIndex = 0;
- _freeList = -1;
- }
- else
- {
- _freeList = i;
- }
- return true;
- }
-
- /// <summary>
- /// Number of elements in this hashset
- /// </summary>
- public int Count
- {
- get { return _count; }
- }
-
- /// <summary>
- /// Whether this is readonly
- /// </summary>
- bool ICollection<T>.IsReadOnly
- {
- get { return false; }
- }
-
- #endregion
-
- #region IEnumerable methods
-
- public Enumerator GetEnumerator()
- {
- return new Enumerator(this);
- }
-
- IEnumerator<T> IEnumerable<T>.GetEnumerator()
- {
- return new Enumerator(this);
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return new Enumerator(this);
- }
-
- #endregion
-
- #region ISerializable methods
-
- public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
- {
- if (info == null)
- {
- throw new ArgumentNullException(nameof(info));
- }
-
- info.AddValue(VersionName, _version); // need to serialize version to avoid problems with serializing while enumerating
- info.AddValue(ComparerName, _comparer ?? EqualityComparer<T>.Default, typeof(IEqualityComparer<T>));
- info.AddValue(CapacityName, _buckets == null ? 0 : _buckets.Length);
-
- if (_buckets != null)
- {
- T[] array = new T[_count];
- CopyTo(array);
- info.AddValue(ElementsName, array, typeof(T[]));
- }
- }
-
- #endregion
-
- #region IDeserializationCallback methods
-
- public virtual void OnDeserialization(object? sender)
- {
- if (_siInfo == null)
- {
- // It might be necessary to call OnDeserialization from a container if the
- // container object also implements OnDeserialization. We can return immediately
- // if this function is called twice. Note we set _siInfo to null at the end of this method.
- return;
- }
-
- int capacity = _siInfo.GetInt32(CapacityName);
- _comparer = (IEqualityComparer<T>)_siInfo.GetValue(ComparerName, typeof(IEqualityComparer<T>))!;
- _freeList = -1;
-
- if (capacity != 0)
- {
- _buckets = new int[capacity];
- _slots = new Slot[capacity];
-
- T[]? array = (T[]?)_siInfo.GetValue(ElementsName, typeof(T[]));
-
- if (array == null)
- {
- throw new SerializationException(SR.Serialization_MissingKeys);
- }
-
- // there are no resizes here because we already set capacity above
- for (int i = 0; i < array.Length; i++)
- {
- AddIfNotPresent(array[i]);
- }
- }
- else
- {
- _buckets = null;
- }
-
- _version = _siInfo.GetInt32(VersionName);
- _siInfo = null;
- }
-
- #endregion
-
- #region HashSet methods
-
- /// <summary>
- /// Add item to this HashSet. Returns bool indicating whether item was added (won't be
- /// added if already present)
- /// </summary>
- /// <param name="item"></param>
- /// <returns>true if added, false if already present</returns>
- public bool Add(T item)
- {
- return AddIfNotPresent(item);
- }
-
- /// <summary>
- /// Searches the set for a given value and returns the equal value it finds, if any.
- /// </summary>
- /// <param name="equalValue">The value to search for.</param>
- /// <param name="actualValue">The value from the set that the search found, or the default value of <typeparamref name="T"/> when the search yielded no match.</param>
- /// <returns>A value indicating whether the search was successful.</returns>
- /// <remarks>
- /// This can be useful when you want to reuse a previously stored reference instead of
- /// a newly constructed one (so that more sharing of references can occur) or to look up
- /// a value that has more complete data than the value you currently have, although their
- /// comparer functions indicate they are equal.
- /// </remarks>
- public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue)
- {
- if (_buckets != null)
- {
- int i = InternalIndexOf(equalValue);
- if (i >= 0)
- {
- actualValue = _slots[i].value;
- return true;
- }
- }
- actualValue = default!;
- return false;
- }
-
- /// <summary>
- /// Take the union of this HashSet with other. Modifies this set.
- ///
- /// Implementation note: GetSuggestedCapacity (to increase capacity in advance avoiding
- /// multiple resizes ended up not being useful in practice; quickly gets to the
- /// point where it's a wasteful check.
- /// </summary>
- /// <param name="other">enumerable with items to add</param>
- public void UnionWith(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- foreach (T item in other)
- {
- AddIfNotPresent(item);
- }
- }
-
- /// <summary>
- /// Takes the intersection of this set with other. Modifies this set.
- ///
- /// Implementation Notes:
- /// We get better perf if other is a hashset using same equality comparer, because we
- /// get constant contains check in other. Resulting cost is O(n1) to iterate over this.
- ///
- /// If we can't go above route, iterate over the other and mark intersection by checking
- /// contains in this. Then loop over and delete any unmarked elements. Total cost is n2+n1.
- ///
- /// Attempts to return early based on counts alone, using the property that the
- /// intersection of anything with the empty set is the empty set.
- /// </summary>
- /// <param name="other">enumerable with items to add </param>
- public void IntersectWith(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // intersection of anything with empty set is empty set, so return if count is 0
- if (_count == 0)
- {
- return;
- }
-
- // set intersecting with itself is the same set
- if (other == this)
- {
- return;
- }
-
- // if other is empty, intersection is empty set; remove all elements and we're done
- // can only figure this out if implements ICollection<T>. (IEnumerable<T> has no count)
- ICollection<T>? otherAsCollection = other as ICollection<T>;
- if (otherAsCollection != null)
- {
- if (otherAsCollection.Count == 0)
- {
- Clear();
- return;
- }
-
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // faster if other is a hashset using same equality comparer; so check
- // that other is a hashset using the same equality comparer.
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- IntersectWithHashSetWithSameEC(otherAsSet);
- return;
- }
- }
-
- IntersectWithEnumerable(other);
- }
-
- /// <summary>
- /// Remove items in other from this set. Modifies this set.
- /// </summary>
- /// <param name="other">enumerable with items to remove</param>
- public void ExceptWith(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // this is already the empty set; return
- if (_count == 0)
- {
- return;
- }
-
- // special case if other is this; a set minus itself is the empty set
- if (other == this)
- {
- Clear();
- return;
- }
-
- // remove every element in other from this
- foreach (T element in other)
- {
- Remove(element);
- }
- }
-
- /// <summary>
- /// Takes symmetric difference (XOR) with other and this set. Modifies this set.
- /// </summary>
- /// <param name="other">enumerable with items to XOR</param>
- public void SymmetricExceptWith(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // if set is empty, then symmetric difference is other
- if (_count == 0)
- {
- UnionWith(other);
- return;
- }
-
- // special case this; the symmetric difference of a set with itself is the empty set
- if (other == this)
- {
- Clear();
- return;
- }
-
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // If other is a HashSet, it has unique elements according to its equality comparer,
- // but if they're using different equality comparers, then assumption of uniqueness
- // will fail. So first check if other is a hashset using the same equality comparer;
- // symmetric except is a lot faster and avoids bit array allocations if we can assume
- // uniqueness
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- SymmetricExceptWithUniqueHashSet(otherAsSet);
- }
- else
- {
- SymmetricExceptWithEnumerable(other);
- }
- }
-
- /// <summary>
- /// Checks if this is a subset of other.
- ///
- /// Implementation Notes:
- /// The following properties are used up-front to avoid element-wise checks:
- /// 1. If this is the empty set, then it's a subset of anything, including the empty set
- /// 2. If other has unique elements according to this equality comparer, and this has more
- /// elements than other, then it can't be a subset.
- ///
- /// Furthermore, if other is a hashset using the same equality comparer, we can use a
- /// faster element-wise check.
- /// </summary>
- /// <param name="other"></param>
- /// <returns>true if this is a subset of other; false if not</returns>
- public bool IsSubsetOf(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // The empty set is a subset of any set
- if (_count == 0)
- {
- return true;
- }
-
- // Set is always a subset of itself
- if (other == this)
- {
- return true;
- }
-
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // faster if other has unique elements according to this equality comparer; so check
- // that other is a hashset using the same equality comparer.
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- // if this has more elements then it can't be a subset
- if (_count > otherAsSet.Count)
- {
- return false;
- }
-
- // already checked that we're using same equality comparer. simply check that
- // each element in this is contained in other.
- return IsSubsetOfHashSetWithSameEC(otherAsSet);
- }
- else
- {
- ElementCount result = CheckUniqueAndUnfoundElements(other, false);
- return (result.uniqueCount == _count && result.unfoundCount >= 0);
- }
- }
-
- /// <summary>
- /// Checks if this is a proper subset of other (i.e. strictly contained in)
- ///
- /// Implementation Notes:
- /// The following properties are used up-front to avoid element-wise checks:
- /// 1. If this is the empty set, then it's a proper subset of a set that contains at least
- /// one element, but it's not a proper subset of the empty set.
- /// 2. If other has unique elements according to this equality comparer, and this has >=
- /// the number of elements in other, then this can't be a proper subset.
- ///
- /// Furthermore, if other is a hashset using the same equality comparer, we can use a
- /// faster element-wise check.
- /// </summary>
- /// <param name="other"></param>
- /// <returns>true if this is a proper subset of other; false if not</returns>
- public bool IsProperSubsetOf(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // no set is a proper subset of itself.
- if (other == this)
- {
- return false;
- }
-
- ICollection<T>? otherAsCollection = other as ICollection<T>;
- if (otherAsCollection != null)
- {
- // no set is a proper subset of an empty set
- if (otherAsCollection.Count == 0)
- {
- return false;
- }
-
- // the empty set is a proper subset of anything but the empty set
- if (_count == 0)
- {
- return otherAsCollection.Count > 0;
- }
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // faster if other is a hashset (and we're using same equality comparer)
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- if (_count >= otherAsSet.Count)
- {
- return false;
- }
- // this has strictly less than number of items in other, so the following
- // check suffices for proper subset.
- return IsSubsetOfHashSetWithSameEC(otherAsSet);
- }
- }
-
- ElementCount result = CheckUniqueAndUnfoundElements(other, false);
- return (result.uniqueCount == _count && result.unfoundCount > 0);
- }
-
- /// <summary>
- /// Checks if this is a superset of other
- ///
- /// Implementation Notes:
- /// The following properties are used up-front to avoid element-wise checks:
- /// 1. If other has no elements (it's the empty set), then this is a superset, even if this
- /// is also the empty set.
- /// 2. If other has unique elements according to this equality comparer, and this has less
- /// than the number of elements in other, then this can't be a superset
- ///
- /// </summary>
- /// <param name="other"></param>
- /// <returns>true if this is a superset of other; false if not</returns>
- public bool IsSupersetOf(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // a set is always a superset of itself
- if (other == this)
- {
- return true;
- }
-
- // try to fall out early based on counts
- ICollection<T>? otherAsCollection = other as ICollection<T>;
- if (otherAsCollection != null)
- {
- // if other is the empty set then this is a superset
- if (otherAsCollection.Count == 0)
- {
- return true;
- }
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // try to compare based on counts alone if other is a hashset with
- // same equality comparer
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- if (otherAsSet.Count > _count)
- {
- return false;
- }
- }
- }
-
- return ContainsAllElements(other);
- }
-
- /// <summary>
- /// Checks if this is a proper superset of other (i.e. other strictly contained in this)
- ///
- /// Implementation Notes:
- /// This is slightly more complicated than above because we have to keep track if there
- /// was at least one element not contained in other.
- ///
- /// The following properties are used up-front to avoid element-wise checks:
- /// 1. If this is the empty set, then it can't be a proper superset of any set, even if
- /// other is the empty set.
- /// 2. If other is an empty set and this contains at least 1 element, then this is a proper
- /// superset.
- /// 3. If other has unique elements according to this equality comparer, and other's count
- /// is greater than or equal to this count, then this can't be a proper superset
- ///
- /// Furthermore, if other has unique elements according to this equality comparer, we can
- /// use a faster element-wise check.
- /// </summary>
- /// <param name="other"></param>
- /// <returns>true if this is a proper superset of other; false if not</returns>
- public bool IsProperSupersetOf(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // the empty set isn't a proper superset of any set.
- if (_count == 0)
- {
- return false;
- }
-
- // a set is never a strict superset of itself
- if (other == this)
- {
- return false;
- }
-
- ICollection<T>? otherAsCollection = other as ICollection<T>;
- if (otherAsCollection != null)
- {
- // if other is the empty set then this is a superset
- if (otherAsCollection.Count == 0)
- {
- // note that this has at least one element, based on above check
- return true;
- }
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // faster if other is a hashset with the same equality comparer
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- if (otherAsSet.Count >= _count)
- {
- return false;
- }
- // now perform element check
- return ContainsAllElements(otherAsSet);
- }
- }
- // couldn't fall out in the above cases; do it the long way
- ElementCount result = CheckUniqueAndUnfoundElements(other, true);
- return (result.uniqueCount < _count && result.unfoundCount == 0);
- }
-
- /// <summary>
- /// Checks if this set overlaps other (i.e. they share at least one item)
- /// </summary>
- /// <param name="other"></param>
- /// <returns>true if these have at least one common element; false if disjoint</returns>
- public bool Overlaps(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- if (_count == 0)
- {
- return false;
- }
-
- // set overlaps itself
- if (other == this)
- {
- return true;
- }
-
- foreach (T element in other)
- {
- if (Contains(element))
- {
- return true;
- }
- }
- return false;
- }
-
- /// <summary>
- /// Checks if this and other contain the same elements. This is set equality:
- /// duplicates and order are ignored
- /// </summary>
- /// <param name="other"></param>
- /// <returns></returns>
- public bool SetEquals(IEnumerable<T> other)
- {
- if (other == null)
- {
- throw new ArgumentNullException(nameof(other));
- }
-
- // a set is equal to itself
- if (other == this)
- {
- return true;
- }
-
- HashSet<T>? otherAsSet = other as HashSet<T>;
- // faster if other is a hashset and we're using same equality comparer
- if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet))
- {
- // attempt to return early: since both contain unique elements, if they have
- // different counts, then they can't be equal
- if (_count != otherAsSet.Count)
- {
- return false;
- }
-
- // already confirmed that the sets have the same number of distinct elements, so if
- // one is a superset of the other then they must be equal
- return ContainsAllElements(otherAsSet);
- }
- else
- {
- ICollection<T>? otherAsCollection = other as ICollection<T>;
- if (otherAsCollection != null)
- {
- // if this count is 0 but other contains at least one element, they can't be equal
- if (_count == 0 && otherAsCollection.Count > 0)
- {
- return false;
- }
- }
- ElementCount result = CheckUniqueAndUnfoundElements(other, true);
- return (result.uniqueCount == _count && result.unfoundCount == 0);
- }
- }
-
- public void CopyTo(T[] array)
- {
- CopyTo(array, 0, _count);
- }
-
- public void CopyTo(T[] array, int arrayIndex, int count)
- {
- if (array == null)
- {
- throw new ArgumentNullException(nameof(array));
- }
-
- // check array index valid index into array
- if (arrayIndex < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, SR.ArgumentOutOfRange_NeedNonNegNum);
- }
-
- // also throw if count less than 0
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(count), count, SR.ArgumentOutOfRange_NeedNonNegNum);
- }
-
- // will array, starting at arrayIndex, be able to hold elements? Note: not
- // checking arrayIndex >= array.Length (consistency with list of allowing
- // count of 0; subsequent check takes care of the rest)
- if (arrayIndex > array.Length || count > array.Length - arrayIndex)
- {
- throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall);
- }
-
- int numCopied = 0;
- for (int i = 0; i < _lastIndex && numCopied < count; i++)
- {
- if (_slots[i].hashCode >= 0)
- {
- array[arrayIndex + numCopied] = _slots[i].value;
- numCopied++;
- }
- }
- }
-
- /// <summary>
- /// Remove elements that match specified predicate. Returns the number of elements removed
- /// </summary>
- /// <param name="match"></param>
- /// <returns></returns>
- public int RemoveWhere(Predicate<T> match)
- {
- if (match == null)
- {
- throw new ArgumentNullException(nameof(match));
- }
-
- int numRemoved = 0;
- for (int i = 0; i < _lastIndex; i++)
- {
- if (_slots[i].hashCode >= 0)
- {
- // cache value in case delegate removes it
- T value = _slots[i].value;
- if (match(value))
- {
- // check again that remove actually removed it
- if (Remove(value))
- {
- numRemoved++;
- }
- }
- }
- }
- return numRemoved;
- }
-
- /// <summary>
- /// Gets the IEqualityComparer that is used to determine equality of keys for
- /// the HashSet.
- /// </summary>
- public IEqualityComparer<T> Comparer
- {
- get
- {
- return _comparer ?? EqualityComparer<T>.Default;
- }
- }
-
- /// <summary>
- /// Ensures that the hash set can hold up to 'capacity' entries without any further expansion of its backing storage.
- /// </summary>
- public int EnsureCapacity(int capacity)
- {
- if (capacity < 0)
- throw new ArgumentOutOfRangeException(nameof(capacity));
- int currentCapacity = _slots == null ? 0 : _slots.Length;
- if (currentCapacity >= capacity)
- return currentCapacity;
- if (_buckets == null)
- return Initialize(capacity);
-
- int newSize = HashHelpers.GetPrime(capacity);
- SetCapacity(newSize);
- return newSize;
- }
-
- /// <summary>
- /// Sets the capacity of this list to the size of the list (rounded up to nearest prime),
- /// unless count is 0, in which case we release references.
- ///
- /// This method can be used to minimize a list's memory overhead once it is known that no
- /// new elements will be added to the list. To completely clear a list and release all
- /// memory referenced by the list, execute the following statements:
- ///
- /// list.Clear();
- /// list.TrimExcess();
- /// </summary>
- public void TrimExcess()
- {
- Debug.Assert(_count >= 0, "_count is negative");
-
- if (_count == 0)
- {
- // if count is zero, clear references
- _buckets = null;
- _slots = null!;
- _version++;
- }
- else
- {
- Debug.Assert(_buckets != null, "_buckets was null but _count > 0");
-
- // similar to IncreaseCapacity but moves down elements in case add/remove/etc
- // caused fragmentation
- int newSize = HashHelpers.GetPrime(_count);
- Slot[] newSlots = new Slot[newSize];
- int[] newBuckets = new int[newSize];
-
- // move down slots and rehash at the same time. newIndex keeps track of current
- // position in newSlots array
- int newIndex = 0;
- for (int i = 0; i < _lastIndex; i++)
- {
- if (_slots[i].hashCode >= 0)
- {
- newSlots[newIndex] = _slots[i];
-
- // rehash
- int bucket = newSlots[newIndex].hashCode % newSize;
- newSlots[newIndex].next = newBuckets[bucket] - 1;
- newBuckets[bucket] = newIndex + 1;
-
- newIndex++;
- }
- }
-
- Debug.Assert(newSlots.Length <= _slots.Length, "capacity increased after TrimExcess");
-
- _lastIndex = newIndex;
- _slots = newSlots;
- _buckets = newBuckets;
- _freeList = -1;
- }
- }
-
- #endregion
-
- #region Helper methods
-
- /// <summary>
- /// Used for deep equality of HashSet testing
- /// </summary>
- /// <returns></returns>
- public static IEqualityComparer<HashSet<T>> CreateSetComparer()
- {
- return new HashSetEqualityComparer<T>();
- }
-
- /// <summary>
- /// Initializes buckets and slots arrays. Uses suggested capacity by finding next prime
- /// greater than or equal to capacity.
- /// </summary>
- /// <param name="capacity"></param>
- private int Initialize(int capacity)
- {
- Debug.Assert(_buckets == null, "Initialize was called but _buckets was non-null");
-
- int size = HashHelpers.GetPrime(capacity);
-
- _buckets = new int[size];
- _slots = new Slot[size];
- return size;
- }
-
- /// <summary>
- /// Expand to new capacity. New capacity is next prime greater than or equal to suggested
- /// size. This is called when the underlying array is filled. This performs no
- /// defragmentation, allowing faster execution; note that this is reasonable since
- /// AddIfNotPresent attempts to insert new elements in re-opened spots.
- /// </summary>
- private void IncreaseCapacity()
- {
- Debug.Assert(_buckets != null, "IncreaseCapacity called on a set with no elements");
-
- int newSize = HashHelpers.ExpandPrime(_count);
- if (newSize <= _count)
- {
- throw new ArgumentException(SR.Arg_HSCapacityOverflow);
- }
-
- // Able to increase capacity; copy elements to larger array and rehash
- SetCapacity(newSize);
- }
-
- /// <summary>
- /// Set the underlying buckets array to size newSize and rehash. Note that newSize
- /// *must* be a prime. It is very likely that you want to call IncreaseCapacity()
- /// instead of this method.
- /// </summary>
- private void SetCapacity(int newSize)
- {
- Debug.Assert(HashHelpers.IsPrime(newSize), "New size is not prime!");
- Debug.Assert(_buckets != null, "SetCapacity called on a set with no elements");
-
- Slot[] newSlots = new Slot[newSize];
- if (_slots != null)
- {
- Array.Copy(_slots, newSlots, _lastIndex);
- }
-
- int[] newBuckets = new int[newSize];
- for (int i = 0; i < _lastIndex; i++)
- {
- int hashCode = newSlots[i].hashCode;
- if (hashCode >= 0)
- {
- int bucket = hashCode % newSize;
- newSlots[i].next = newBuckets[bucket] - 1;
- newBuckets[bucket] = i + 1;
- }
- }
- _slots = newSlots;
- _buckets = newBuckets;
- }
-
- /// <summary>
- /// Adds value to HashSet if not contained already
- /// Returns true if added and false if already present
- /// </summary>
- /// <param name="value">value to find</param>
- /// <returns></returns>
- private bool AddIfNotPresent(T value)
- {
- if (_buckets == null)
- {
- Initialize(0);
- }
-
- int hashCode;
- int bucket;
- int collisionCount = 0;
- Slot[] slots = _slots;
-
- IEqualityComparer<T>? comparer = _comparer;
-
- if (comparer == null)
- {
- hashCode = value == null ? 0 : InternalGetHashCode(value.GetHashCode());
- bucket = hashCode % _buckets!.Length;
-
- if (typeof(T).IsValueType)
- {
- for (int i = _buckets[bucket] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && EqualityComparer<T>.Default.Equals(slots[i].value, value))
- {
- return false;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- else
- {
- // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize
- // https://github.com/dotnet/runtime/issues/10050
- // So cache in a local rather than get EqualityComparer per loop iteration
- EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
-
- for (int i = _buckets[bucket] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && defaultComparer.Equals(slots[i].value, value))
- {
- return false;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- }
- else
- {
- hashCode = value == null ? 0 : InternalGetHashCode(comparer.GetHashCode(value));
- bucket = hashCode % _buckets!.Length;
-
- for (int i = _buckets[bucket] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, value))
- {
- return false;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
-
- int index;
- if (_freeList >= 0)
- {
- index = _freeList;
- _freeList = slots[index].next;
- }
- else
- {
- if (_lastIndex == slots.Length)
- {
- IncreaseCapacity();
- // this will change during resize
- slots = _slots;
- bucket = hashCode % _buckets.Length;
- }
- index = _lastIndex;
- _lastIndex++;
- }
- slots[index].hashCode = hashCode;
- slots[index].value = value;
- slots[index].next = _buckets[bucket] - 1;
- _buckets[bucket] = index + 1;
- _count++;
- _version++;
-
- return true;
- }
-
- // Add value at known index with known hash code. Used only
- // when constructing from another HashSet.
- private void AddValue(int index, int hashCode, T value)
- {
- int bucket = hashCode % _buckets!.Length;
-
-#if DEBUG
- IEqualityComparer<T> comparer = _comparer ?? EqualityComparer<T>.Default;
- Debug.Assert(InternalGetHashCode(value, comparer) == hashCode);
- for (int i = _buckets[bucket] - 1; i >= 0; i = _slots[i].next)
- {
- Debug.Assert(!comparer.Equals(_slots[i].value, value));
- }
-#endif
-
- Debug.Assert(_freeList == -1);
- _slots[index].hashCode = hashCode;
- _slots[index].value = value;
- _slots[index].next = _buckets[bucket] - 1;
- _buckets[bucket] = index + 1;
- }
-
- /// <summary>
- /// Checks if this contains of other's elements. Iterates over other's elements and
- /// returns false as soon as it finds an element in other that's not in this.
- /// Used by SupersetOf, ProperSupersetOf, and SetEquals.
- /// </summary>
- /// <param name="other"></param>
- /// <returns></returns>
- private bool ContainsAllElements(IEnumerable<T> other)
- {
- foreach (T element in other)
- {
- if (!Contains(element))
- {
- return false;
- }
- }
- return true;
- }
-
- /// <summary>
- /// Implementation Notes:
- /// If other is a hashset and is using same equality comparer, then checking subset is
- /// faster. Simply check that each element in this is in other.
- ///
- /// Note: if other doesn't use same equality comparer, then Contains check is invalid,
- /// which is why callers must take are of this.
- ///
- /// If callers are concerned about whether this is a proper subset, they take care of that.
- ///
- /// </summary>
- /// <param name="other"></param>
- /// <returns></returns>
- private bool IsSubsetOfHashSetWithSameEC(HashSet<T> other)
- {
- foreach (T item in this)
- {
- if (!other.Contains(item))
- {
- return false;
- }
- }
- return true;
- }
-
- /// <summary>
- /// If other is a hashset that uses same equality comparer, intersect is much faster
- /// because we can use other's Contains
- /// </summary>
- /// <param name="other"></param>
- private void IntersectWithHashSetWithSameEC(HashSet<T> other)
- {
- for (int i = 0; i < _lastIndex; i++)
- {
- if (_slots[i].hashCode >= 0)
- {
- T item = _slots[i].value;
- if (!other.Contains(item))
- {
- Remove(item);
- }
- }
- }
- }
-
- /// <summary>
- /// Iterate over other. If contained in this, mark an element in bit array corresponding to
- /// its position in _slots. If anything is unmarked (in bit array), remove it.
- ///
- /// This attempts to allocate on the stack, if below StackAllocThreshold.
- /// </summary>
- /// <param name="other"></param>
- private unsafe void IntersectWithEnumerable(IEnumerable<T> other)
- {
- Debug.Assert(_buckets != null, "_buckets shouldn't be null; callers should check first");
-
- // keep track of current last index; don't want to move past the end of our bit array
- // (could happen if another thread is modifying the collection)
- int originalLastIndex = _lastIndex;
- int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex);
-
- Span<int> span = stackalloc int[StackAllocThreshold];
- BitHelper bitHelper = intArrayLength <= StackAllocThreshold ?
- new BitHelper(span.Slice(0, intArrayLength), clear: true) :
- new BitHelper(new int[intArrayLength], clear: false);
-
- // mark if contains: find index of in slots array and mark corresponding element in bit array
- foreach (T item in other)
- {
- int index = InternalIndexOf(item);
- if (index >= 0)
- {
- bitHelper.MarkBit(index);
- }
- }
-
- // if anything unmarked, remove it. Perf can be optimized here if BitHelper had a
- // FindFirstUnmarked method.
- for (int i = 0; i < originalLastIndex; i++)
- {
- if (_slots[i].hashCode >= 0 && !bitHelper.IsMarked(i))
- {
- Remove(_slots[i].value);
- }
- }
- }
-
- /// <summary>
- /// Used internally by set operations which have to rely on bit array marking. This is like
- /// Contains but returns index in slots array.
- /// </summary>
- /// <param name="item"></param>
- /// <returns></returns>
- private int InternalIndexOf(T item)
- {
- Debug.Assert(_buckets != null, "_buckets was null; callers should check first");
-
- int[]? buckets = _buckets;
- int collisionCount = 0;
- Slot[] slots = _slots;
- IEqualityComparer<T>? comparer = _comparer;
-
- if (comparer == null)
- {
- int hashCode = item == null ? 0 : InternalGetHashCode(item.GetHashCode());
-
- if (typeof(T).IsValueType)
- {
- // see note at "HashSet" level describing why "- 1" appears in for loop
- for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && EqualityComparer<T>.Default.Equals(slots[i].value, item))
- {
- return i;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- else
- {
- // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize
- // https://github.com/dotnet/runtime/issues/10050
- // So cache in a local rather than get EqualityComparer per loop iteration
- EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
-
- // see note at "HashSet" level describing why "- 1" appears in for loop
- for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && defaultComparer.Equals(slots[i].value, item))
- {
- return i;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- }
- else
- {
- int hashCode = item == null ? 0 : InternalGetHashCode(comparer.GetHashCode(item));
-
- // see note at "HashSet" level describing why "- 1" appears in for loop
- for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, item))
- {
- return i;
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- }
- // wasn't found
- return -1;
- }
-
- /// <summary>
- /// if other is a set, we can assume it doesn't have duplicate elements, so use this
- /// technique: if can't remove, then it wasn't present in this set, so add.
- ///
- /// As with other methods, callers take care of ensuring that other is a hashset using the
- /// same equality comparer.
- /// </summary>
- /// <param name="other"></param>
- private void SymmetricExceptWithUniqueHashSet(HashSet<T> other)
- {
- foreach (T item in other)
- {
- if (!Remove(item))
- {
- AddIfNotPresent(item);
- }
- }
- }
-
- /// <summary>
- /// Implementation notes:
- ///
- /// Used for symmetric except when other isn't a HashSet. This is more tedious because
- /// other may contain duplicates. HashSet technique could fail in these situations:
- /// 1. Other has a duplicate that's not in this: HashSet technique would add then
- /// remove it.
- /// 2. Other has a duplicate that's in this: HashSet technique would remove then add it
- /// back.
- /// In general, its presence would be toggled each time it appears in other.
- ///
- /// This technique uses bit marking to indicate whether to add/remove the item. If already
- /// present in collection, it will get marked for deletion. If added from other, it will
- /// get marked as something not to remove.
- ///
- /// </summary>
- /// <param name="other"></param>
- private unsafe void SymmetricExceptWithEnumerable(IEnumerable<T> other)
- {
- int originalLastIndex = _lastIndex;
- int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex);
-
- Span<int> itemsToRemoveSpan = stackalloc int[StackAllocThreshold / 2];
- BitHelper itemsToRemove = intArrayLength <= StackAllocThreshold / 2 ?
- new BitHelper(itemsToRemoveSpan.Slice(0, intArrayLength), clear: true) :
- new BitHelper(new int[intArrayLength], clear: false);
-
- Span<int> itemsAddedFromOtherSpan = stackalloc int[StackAllocThreshold / 2];
- BitHelper itemsAddedFromOther = intArrayLength <= StackAllocThreshold / 2 ?
- new BitHelper(itemsAddedFromOtherSpan.Slice(0, intArrayLength), clear: true) :
- new BitHelper(new int[intArrayLength], clear: false);
-
- foreach (T item in other)
- {
- int location = 0;
- bool added = AddOrGetLocation(item, out location);
- if (added)
- {
- // wasn't already present in collection; flag it as something not to remove
- // *NOTE* if location is out of range, we should ignore. BitHelper will
- // detect that it's out of bounds and not try to mark it. But it's
- // expected that location could be out of bounds because adding the item
- // will increase _lastIndex as soon as all the free spots are filled.
- itemsAddedFromOther.MarkBit(location);
- }
- else
- {
- // already there...if not added from other, mark for remove.
- // *NOTE* Even though BitHelper will check that location is in range, we want
- // to check here. There's no point in checking items beyond originalLastIndex
- // because they could not have been in the original collection
- if (location < originalLastIndex && !itemsAddedFromOther.IsMarked(location))
- {
- itemsToRemove.MarkBit(location);
- }
- }
- }
-
- // if anything marked, remove it
- for (int i = 0; i < originalLastIndex; i++)
- {
- if (itemsToRemove.IsMarked(i))
- {
- Remove(_slots[i].value);
- }
- }
- }
-
- /// <summary>
- /// Add if not already in hashset. Returns an out param indicating index where added. This
- /// is used by SymmetricExcept because it needs to know the following things:
- /// - whether the item was already present in the collection or added from other
- /// - where it's located (if already present, it will get marked for removal, otherwise
- /// marked for keeping)
- /// </summary>
- /// <param name="value"></param>
- /// <param name="location"></param>
- /// <returns></returns>
- private bool AddOrGetLocation(T value, out int location)
- {
- Debug.Assert(_buckets != null, "_buckets is null, callers should have checked");
-
- IEqualityComparer<T>? comparer = _comparer;
- int hashCode = InternalGetHashCode(value, comparer);
- int bucket = hashCode % _buckets.Length;
- int collisionCount = 0;
- Slot[] slots = _slots;
- for (int i = _buckets[bucket] - 1; i >= 0; i = slots[i].next)
- {
- if (slots[i].hashCode == hashCode && (comparer?.Equals(slots[i].value, value) ?? EqualityComparer<T>.Default.Equals(slots[i].value, value)))
- {
- location = i;
- return false; //already present
- }
-
- if (collisionCount >= slots.Length)
- {
- // The chain of entries forms a loop, which means a concurrent update has happened.
- throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
- }
- collisionCount++;
- }
- int index;
- if (_freeList >= 0)
- {
- index = _freeList;
- _freeList = slots[index].next;
- }
- else
- {
- if (_lastIndex == slots.Length)
- {
- IncreaseCapacity();
- // this will change during resize
- slots = _slots;
- bucket = hashCode % _buckets.Length;
- }
- index = _lastIndex;
- _lastIndex++;
- }
- slots[index].hashCode = hashCode;
- slots[index].value = value;
- slots[index].next = _buckets[bucket] - 1;
- _buckets[bucket] = index + 1;
- _count++;
- _version++;
- location = index;
- return true;
- }
-
- /// <summary>
- /// Determines counts that can be used to determine equality, subset, and superset. This
- /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet
- /// these properties can be checked faster without use of marking because we can assume
- /// other has no duplicates.
- ///
- /// The following count checks are performed by callers:
- /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything
- /// in other is in this and everything in this is in other
- /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may
- /// have elements not in this and everything in this is in other
- /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e
- /// other must have at least one element not in this and everything in this is in other
- /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less
- /// than _count; i.e. everything in other was in this and this had at least one element
- /// not contained in other.
- ///
- /// An earlier implementation used delegates to perform these checks rather than returning
- /// an ElementCount struct; however this was changed due to the perf overhead of delegates.
- /// </summary>
- /// <param name="other"></param>
- /// <param name="returnIfUnfound">Allows us to finish faster for equals and proper superset
- /// because unfoundCount must be 0.</param>
- /// <returns></returns>
- private unsafe ElementCount CheckUniqueAndUnfoundElements(IEnumerable<T> other, bool returnIfUnfound)
- {
- ElementCount result;
-
- // need special case in case this has no elements.
- if (_count == 0)
- {
- int numElementsInOther = 0;
- foreach (T item in other)
- {
- numElementsInOther++;
- // break right away, all we want to know is whether other has 0 or 1 elements
- break;
- }
- result.uniqueCount = 0;
- result.unfoundCount = numElementsInOther;
- return result;
- }
-
- Debug.Assert((_buckets != null) && (_count > 0), "_buckets was null but count greater than 0");
-
- int originalLastIndex = _lastIndex;
- int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex);
-
- Span<int> span = stackalloc int[StackAllocThreshold];
- BitHelper bitHelper = intArrayLength <= StackAllocThreshold ?
- new BitHelper(span.Slice(0, intArrayLength), clear: true) :
- new BitHelper(new int[intArrayLength], clear: false);
-
- // count of items in other not found in this
- int unfoundCount = 0;
- // count of unique items in other found in this
- int uniqueFoundCount = 0;
-
- foreach (T item in other)
- {
- int index = InternalIndexOf(item);
- if (index >= 0)
- {
- if (!bitHelper.IsMarked(index))
- {
- // item hasn't been seen yet
- bitHelper.MarkBit(index);
- uniqueFoundCount++;
- }
- }
- else
- {
- unfoundCount++;
- if (returnIfUnfound)
- {
- break;
- }
- }
- }
-
- result.uniqueCount = uniqueFoundCount;
- result.unfoundCount = unfoundCount;
- return result;
- }
-
-
- /// <summary>
- /// Internal method used for HashSetEqualityComparer. Compares set1 and set2 according
- /// to specified comparer.
- ///
- /// Because items are hashed according to a specific equality comparer, we have to resort
- /// to n^2 search if they're using different equality comparers.
- /// </summary>
- /// <param name="set1"></param>
- /// <param name="set2"></param>
- /// <param name="comparer"></param>
- /// <returns></returns>
- internal static bool HashSetEquals(HashSet<T>? set1, HashSet<T>? set2, IEqualityComparer<T> comparer)
- {
- // handle null cases first
- if (set1 == null)
- {
- return (set2 == null);
- }
- else if (set2 == null)
- {
- // set1 != null
- return false;
- }
-
- // all comparers are the same; this is faster
- if (AreEqualityComparersEqual(set1, set2))
- {
- if (set1.Count != set2.Count)
- {
- return false;
- }
- // suffices to check subset
- foreach (T item in set2)
- {
- if (!set1.Contains(item))
- {
- return false;
- }
- }
- return true;
- }
- else
- { // n^2 search because items are hashed according to their respective ECs
- foreach (T set2Item in set2)
- {
- bool found = false;
- foreach (T set1Item in set1)
- {
- if (comparer.Equals(set2Item, set1Item))
- {
- found = true;
- break;
- }
- }
- if (!found)
- {
- return false;
- }
- }
- return true;
- }
- }
-
- /// <summary>
- /// Checks if equality comparers are equal. This is used for algorithms that can
- /// speed up if it knows the other item has unique elements. I.e. if they're using
- /// different equality comparers, then uniqueness assumption between sets break.
- /// </summary>
- /// <param name="set1"></param>
- /// <param name="set2"></param>
- /// <returns></returns>
- private static bool AreEqualityComparersEqual(HashSet<T> set1, HashSet<T> set2)
- {
- return set1.Comparer.Equals(set2.Comparer);
- }
-
- /// <summary>
- /// Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
- /// </summary>
- /// <param name="item"></param>
- /// <param name="comparer"></param>
- /// <returns>hash code</returns>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int InternalGetHashCode(T item, IEqualityComparer<T>? comparer)
- {
- if (item == null)
- {
- return 0;
- }
-
- int hashCode = comparer?.GetHashCode(item) ?? item.GetHashCode();
- return hashCode & Lower31BitMask;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private int InternalGetHashCode(int hashCode)
- {
- return hashCode & Lower31BitMask;
- }
-
- #endregion
-
- // used for set checking operations (using enumerables) that rely on counting
- internal struct ElementCount
- {
- internal int uniqueCount;
- internal int unfoundCount;
- }
-
- internal struct Slot
- {
- internal int hashCode; // Lower 31 bits of hash code, -1 if unused
- internal int next; // Index of next entry, -1 if last
- internal T value;
- }
-
- public struct Enumerator : IEnumerator<T>, IEnumerator
- {
- private readonly HashSet<T> _set;
- private int _index;
- private readonly int _version;
- [AllowNull] private T _current;
-
- internal Enumerator(HashSet<T> set)
- {
- _set = set;
- _index = 0;
- _version = set._version;
- _current = default;
- }
-
- public void Dispose()
- {
- }
-
- public bool MoveNext()
- {
- if (_version != _set._version)
- {
- throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion);
- }
-
- while (_index < _set._lastIndex)
- {
- if (_set._slots[_index].hashCode >= 0)
- {
- _current = _set._slots[_index].value;
- _index++;
- return true;
- }
- _index++;
- }
- _index = _set._lastIndex + 1;
- _current = default;
- return false;
- }
-
- public T Current
- {
- get
- {
- return _current;
- }
- }
-
- object? IEnumerator.Current
- {
- get
- {
- if (_index == 0 || _index == _set._lastIndex + 1)
- {
- throw new InvalidOperationException(SR.InvalidOperation_EnumOpCantHappen);
- }
- return Current;
- }
- }
-
- void IEnumerator.Reset()
- {
- if (_version != _set._version)
- {
- throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion);
- }
-
- _index = 0;
- _current = default;
- }
- }
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace System.Collections.Generic
-{
- /// <summary>
- /// Equality comparer for hashsets of hashsets
- /// </summary>
- /// <typeparam name="T"></typeparam>
- internal sealed class HashSetEqualityComparer<T> : IEqualityComparer<HashSet<T>?>
- {
- private readonly IEqualityComparer<T> _comparer;
-
- public HashSetEqualityComparer()
- {
- _comparer = EqualityComparer<T>.Default;
- }
-
- // using m_comparer to keep equals properties in tact; don't want to choose one of the comparers
- public bool Equals(HashSet<T>? x, HashSet<T>? y)
- {
- return HashSet<T>.HashSetEquals(x, y, _comparer);
- }
-
- public int GetHashCode(HashSet<T>? obj)
- {
- int hashCode = 0;
- if (obj != null)
- {
- foreach (T t in obj)
- {
- if (t != null)
- {
- hashCode ^= (_comparer.GetHashCode(t) & 0x7FFFFFFF);
- }
- }
- } // else returns hashcode of 0 for null hashsets
- return hashCode;
- }
-
- // Equals method for the comparer itself.
- public override bool Equals(object? obj)
- {
- HashSetEqualityComparer<T>? comparer = obj as HashSetEqualityComparer<T>;
- if (comparer == null)
- {
- return false;
- }
- return (_comparer == comparer._comparer);
- }
-
- public override int GetHashCode()
- {
- return _comparer.GetHashCode();
- }
- }
-}
protected override bool Enumerator_Current_UndefinedOperation_Throws => true;
+ protected override ModifyOperation ModifyEnumeratorThrows => PlatformDetection.IsNetFramework ? base.ModifyEnumeratorThrows : (base.ModifyEnumeratorAllowed & ~ModifyOperation.Remove);
+
+ protected override ModifyOperation ModifyEnumeratorAllowed => PlatformDetection.IsNetFramework ? base.ModifyEnumeratorAllowed : ModifyOperation.Overwrite | ModifyOperation.Remove;
+
/// <summary>
/// Returns a set of ModifyEnumerable delegates that modify the enumerable passed to them.
/// </summary>
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Runtime.Serialization.Formatters.Binary;
using Xunit;
namespace System.Collections.Tests
protected override bool ResetImplemented => true;
+ protected override ModifyOperation ModifyEnumeratorThrows => PlatformDetection.IsNetFramework ? base.ModifyEnumeratorThrows : (base.ModifyEnumeratorAllowed & ~(ModifyOperation.Remove | ModifyOperation.Clear));
+
+ protected override ModifyOperation ModifyEnumeratorAllowed => PlatformDetection.IsNetFramework ? base.ModifyEnumeratorAllowed : ModifyOperation.Overwrite | ModifyOperation.Remove | ModifyOperation.Clear;
+
protected override ISet<T> GenericISetFactory()
{
return new HashSet<T>();
}
#endregion
+
+ #region Serialization
+
+ [Fact]
+ public void ComparerSerialization()
+ {
+ // Strings switch between randomized and non-randomized comparers,
+ // however this should never be observable externally.
+ TestComparerSerialization(EqualityComparer<string>.Default);
+
+ // OrdinalCaseSensitiveComparer is internal and (de)serializes as OrdinalComparer
+ TestComparerSerialization(StringComparer.Ordinal, "System.OrdinalComparer");
+
+ // OrdinalIgnoreCaseComparer is internal and (de)serializes as OrdinalComparer
+ TestComparerSerialization(StringComparer.OrdinalIgnoreCase, "System.OrdinalComparer");
+ TestComparerSerialization(StringComparer.CurrentCulture);
+ TestComparerSerialization(StringComparer.CurrentCultureIgnoreCase);
+ TestComparerSerialization(StringComparer.InvariantCulture);
+ TestComparerSerialization(StringComparer.InvariantCultureIgnoreCase);
+
+ // Check other types while here, IEquatable valuetype, nullable valuetype, and non IEquatable object
+ TestComparerSerialization(EqualityComparer<int>.Default);
+ TestComparerSerialization(EqualityComparer<int?>.Default);
+ TestComparerSerialization(EqualityComparer<object>.Default);
+
+ static void TestComparerSerialization<TCompared>(IEqualityComparer<TCompared> equalityComparer, string internalTypeName = null)
+ {
+ var bf = new BinaryFormatter();
+ var s = new MemoryStream();
+
+ var dict = new HashSet<TCompared>(equalityComparer);
+
+ Assert.Same(equalityComparer, dict.Comparer);
+
+ bf.Serialize(s, dict);
+ s.Position = 0;
+ dict = (HashSet<TCompared>)bf.Deserialize(s);
+
+ if (internalTypeName == null)
+ {
+ Assert.IsType(equalityComparer.GetType(), dict.Comparer);
+ }
+ else
+ {
+ Assert.Equal(internalTypeName, dict.Comparer.GetType().ToString());
+ }
+
+ Assert.True(equalityComparer.Equals(dict.Comparer));
+ }
+ }
+
+ #endregion
}
}
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\Comparer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\Dictionary.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\EqualityComparer.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\HashSet.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\HashSetEqualityComparer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IAsyncEnumerable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IAsyncEnumerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\ICollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IEnumerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IEqualityComparer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IList.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\InsertionBehavior.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IReadOnlyCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IReadOnlyDictionary.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Collections\Generic\IReadOnlyList.cs" />
<Compile Include="$(CommonPath)System\Collections\Generic\ReferenceEqualityComparer.cs">
<Link>Common\System\Collections\Generic\ReferenceEqualityComparer.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)System\Collections\Generic\BitHelper.cs">
+ <Link>Common\System\Collections\Generic\BitHelper.cs</Link>
+ </Compile>
<Compile Include="$(CommonPath)System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs">
<Link>Common\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs</Link>
</Compile>
namespace System.Collections.Generic
{
- /// <summary>
- /// Used internally to control behavior of insertion into a <see cref="Dictionary{TKey, TValue}"/>.
- /// </summary>
- internal enum InsertionBehavior : byte
- {
- /// <summary>
- /// The default insertion behavior.
- /// </summary>
- None = 0,
-
- /// <summary>
- /// Specifies that an existing entry with the same key should be overwritten if encountered.
- /// </summary>
- OverwriteExisting = 1,
-
- /// <summary>
- /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown.
- /// </summary>
- ThrowOnExisting = 2
- }
-
[DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback where TKey : notnull
{
- private struct Entry
- {
- public uint hashCode;
- // 0-based index of next entry in chain: -1 means end of chain
- // also encodes whether this entry _itself_ is part of the free list by changing sign and subtracting 3,
- // so -2 means end of free list, -3 means index 0 but on free list, -4 means index 1 but on free list, etc.
- public int next;
- public TKey key; // Key of entry
- public TValue value; // Value of entry
- }
+ // constants for serialization
+ private const string VersionName = "Version"; // Do not rename (binary serialization)
+ private const string HashSizeName = "HashSize"; // Do not rename (binary serialization). Must save buckets.Length
+ private const string KeyValuePairsName = "KeyValuePairs"; // Do not rename (binary serialization)
+ private const string ComparerName = "Comparer"; // Do not rename (binary serialization)
private int[]? _buckets;
private Entry[]? _entries;
private ValueCollection? _values;
private const int StartOfFreeList = -3;
- // constants for serialization
- private const string VersionName = "Version"; // Do not rename (binary serialization)
- private const string HashSizeName = "HashSize"; // Do not rename (binary serialization). Must save buckets.Length
- private const string KeyValuePairsName = "KeyValuePairs"; // Do not rename (binary serialization)
- private const string ComparerName = "Comparer"; // Do not rename (binary serialization)
-
public Dictionary() : this(0, null) { }
public Dictionary(int capacity) : this(capacity, null) { }
public Dictionary(int capacity, IEqualityComparer<TKey>? comparer)
{
- if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
- if (capacity > 0) Initialize(capacity);
+ if (capacity < 0)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
+ }
+
+ if (capacity > 0)
+ {
+ Initialize(capacity);
+ }
+
if (comparer != null && comparer != EqualityComparer<TKey>.Default) // first check for null to avoid forcing default comparer instantiation unnecessarily
{
_comparer = comparer;
public KeyCollection Keys => _keys ??= new KeyCollection(this);
- ICollection<TKey> IDictionary<TKey, TValue>.Keys => _keys ??= new KeyCollection(this);
+ ICollection<TKey> IDictionary<TKey, TValue>.Keys => Keys;
- IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => _keys ??= new KeyCollection(this);
+ IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => Keys;
public ValueCollection Values => _values ??= new ValueCollection(this);
- ICollection<TValue> IDictionary<TKey, TValue>.Values => _values ??= new ValueCollection(this);
+ ICollection<TValue> IDictionary<TKey, TValue>.Values => Values;
- IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => _values ??= new ValueCollection(this);
+ IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => Values;
public TValue this[TKey key]
{
{
return value;
}
+
ThrowHelper.ThrowKeyNotFoundException(key);
return default;
}
Debug.Assert(modified); // If there was an existing key and the Add failed, an exception will already have been thrown.
}
- void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> keyValuePair)
- => Add(keyValuePair.Key, keyValuePair.Value);
+ void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> keyValuePair) =>
+ Add(keyValuePair.Key, keyValuePair.Value);
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> keyValuePair)
{
{
return true;
}
+
return false;
}
Remove(keyValuePair.Key);
return true;
}
+
return false;
}
}
}
- public bool ContainsKey(TKey key)
- => !Unsafe.IsNullRef(ref FindValue(key));
+ public bool ContainsKey(TKey key) =>
+ !Unsafe.IsNullRef(ref FindValue(key));
public bool ContainsValue(TValue value)
{
{
for (int i = 0; i < _count; i++)
{
- if (entries![i].next >= -1 && entries[i].value == null) return true;
+ if (entries![i].next >= -1 && entries[i].value == null)
+ {
+ return true;
+ }
}
}
- else
+ else if (typeof(TValue).IsValueType)
{
- if (typeof(TValue).IsValueType)
+ // ValueType: Devirtualize with EqualityComparer<TValue>.Default intrinsic
+ for (int i = 0; i < _count; i++)
{
- // ValueType: Devirtualize with EqualityComparer<TValue>.Default intrinsic
- for (int i = 0; i < _count; i++)
+ if (entries![i].next >= -1 && EqualityComparer<TValue>.Default.Equals(entries[i].value, value))
{
- if (entries![i].next >= -1 && EqualityComparer<TValue>.Default.Equals(entries[i].value, value)) return true;
+ return true;
}
}
- else
+ }
+ else
+ {
+ // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize
+ // https://github.com/dotnet/runtime/issues/10050
+ // So cache in a local rather than get EqualityComparer per loop iteration
+ EqualityComparer<TValue> defaultComparer = EqualityComparer<TValue>.Default;
+ for (int i = 0; i < _count; i++)
{
- // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize
- // https://github.com/dotnet/runtime/issues/10050
- // So cache in a local rather than get EqualityComparer per loop iteration
- EqualityComparer<TValue> defaultComparer = EqualityComparer<TValue>.Default;
- for (int i = 0; i < _count; i++)
+ if (entries![i].next >= -1 && defaultComparer.Equals(entries[i].value, value))
{
- if (entries![i].next >= -1 && defaultComparer.Equals(entries[i].value, value)) return true;
+ return true;
}
}
}
+
return false;
}
}
}
- public Enumerator GetEnumerator()
- => new Enumerator(this, Enumerator.KeyValuePair);
+ public Enumerator GetEnumerator() => new Enumerator(this, Enumerator.KeyValuePair);
- IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
- => new Enumerator(this, Enumerator.KeyValuePair);
+ IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() =>
+ new Enumerator(this, Enumerator.KeyValuePair);
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
{
// ValueType: Devirtualize with EqualityComparer<TValue>.Default intrinsic
- // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
- i--;
+ i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
do
{
// Should be a while loop https://github.com/dotnet/runtime/issues/9422
collisionCount++;
} while (collisionCount <= (uint)entries.Length);
+
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
goto ConcurrentOperation;
// So cache in a local rather than get EqualityComparer per loop iteration
EqualityComparer<TKey> defaultComparer = EqualityComparer<TKey>.Default;
- // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
- i--;
+ i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
do
{
// Should be a while loop https://github.com/dotnet/runtime/issues/9422
collisionCount++;
} while (collisionCount <= (uint)entries.Length);
+
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
goto ConcurrentOperation;
int i = GetBucket(hashCode);
Entry[]? entries = _entries;
uint collisionCount = 0;
- // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
- i--;
+ i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
do
{
// Should be a while loop https://github.com/dotnet/runtime/issues/9422
collisionCount++;
} while (collisionCount <= (uint)entries.Length);
+
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
goto ConcurrentOperation;
uint collisionCount = 0;
ref int bucket = ref GetBucket(hashCode);
- // Value in _buckets is 1-based
- int i = bucket - 1;
+ int i = bucket - 1; // Value in _buckets is 1-based
if (comparer == null)
{
}
}
- bool updateFreeList = false;
int index;
if (_freeCount > 0)
{
index = _freeList;
- updateFreeList = true;
+ Debug.Assert((StartOfFreeList - entries[_freeList].next) >= -1, "shouldn't overflow because `next` cannot underflow");
+ _freeList = StartOfFreeList - entries[_freeList].next;
_freeCount--;
}
else
}
ref Entry entry = ref entries![index];
-
- if (updateFreeList)
- {
- Debug.Assert((StartOfFreeList - entries[_freeList].next) >= -1, "shouldn't overflow because `next` cannot underflow");
-
- _freeList = StartOfFreeList - entries[_freeList].next;
- }
entry.hashCode = hashCode;
- // Value in _buckets is 1-based
- entry.next = bucket - 1;
+ entry.next = bucket - 1; // Value in _buckets is 1-based
entry.key = key;
- entry.value = value;
- // Value in _buckets is 1-based
+ entry.value = value; // Value in _buckets is 1-based
bucket = index + 1;
_version++;
{
ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_NullKey);
}
+
Add(array[i].Key, array[i].Value);
}
}
HashHelpers.SerializationInfoTable.Remove(this);
}
- private void Resize()
- => Resize(HashHelpers.ExpandPrime(_count), false);
+ private void Resize() => Resize(HashHelpers.ExpandPrime(_count), false);
private void Resize(int newSize, bool forceNewHashCodes)
{
if (entries[i].next >= -1)
{
ref int bucket = ref GetBucket(entries[i].hashCode);
- // Value in _buckets is 1-based
- entries[i].next = bucket - 1;
- // Value in _buckets is 1-based
+ entries[i].next = bucket - 1; // Value in _buckets is 1-based
bucket = i + 1;
}
}
_entries = entries;
}
- // The overload Remove(TKey key, out TValue value) is a copy of this method with one additional
- // statement to copy the value for entry being removed into the output parameter.
- // Code has been intentionally duplicated for performance reasons.
public bool Remove(TKey key)
{
+ // The overload Remove(TKey key, out TValue value) is a copy of this method with one additional
+ // statement to copy the value for entry being removed into the output parameter.
+ // Code has been intentionally duplicated for performance reasons.
+
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
ref int bucket = ref GetBucket(hashCode);
Entry[]? entries = _entries;
int last = -1;
- // Value in buckets is 1-based
- int i = bucket - 1;
+ int i = bucket - 1; // Value in buckets is 1-based
while (i >= 0)
{
ref Entry entry = ref entries[i];
{
if (last < 0)
{
- // Value in buckets is 1-based
- bucket = entry.next + 1;
+ bucket = entry.next + 1; // Value in buckets is 1-based
}
else
{
}
Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646");
-
entry.next = StartOfFreeList - _freeList;
if (RuntimeHelpers.IsReferenceOrContainsReferences<TKey>())
{
entry.key = default!;
}
+
if (RuntimeHelpers.IsReferenceOrContainsReferences<TValue>())
{
entry.value = default!;
}
+
_freeList = i;
_freeCount++;
return true;
return false;
}
- // This overload is a copy of the overload Remove(TKey key) with one additional
- // statement to copy the value for entry being removed into the output parameter.
- // Code has been intentionally duplicated for performance reasons.
public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value)
{
+ // This overload is a copy of the overload Remove(TKey key) with one additional
+ // statement to copy the value for entry being removed into the output parameter.
+ // Code has been intentionally duplicated for performance reasons.
+
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
ref int bucket = ref GetBucket(hashCode);
Entry[]? entries = _entries;
int last = -1;
- // Value in buckets is 1-based
- int i = bucket - 1;
+ int i = bucket - 1; // Value in buckets is 1-based
while (i >= 0)
{
ref Entry entry = ref entries[i];
{
if (last < 0)
{
- // Value in buckets is 1-based
- bucket = entry.next + 1;
+ bucket = entry.next + 1; // Value in buckets is 1-based
}
else
{
value = entry.value;
Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646");
-
entry.next = StartOfFreeList - _freeList;
if (RuntimeHelpers.IsReferenceOrContainsReferences<TKey>())
{
entry.key = default!;
}
+
if (RuntimeHelpers.IsReferenceOrContainsReferences<TValue>())
{
entry.value = default!;
}
+
_freeList = i;
_freeCount++;
return true;
}
}
}
- value = default!;
+
+ value = default;
return false;
}
value = valRef;
return true;
}
- value = default!;
+
+ value = default;
return false;
}
- public bool TryAdd(TKey key, TValue value)
- => TryInsert(key, value, InsertionBehavior.None);
+ public bool TryAdd(TKey key, TValue value) =>
+ TryInsert(key, value, InsertionBehavior.None);
bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly => false;
- void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int index)
- => CopyTo(array, index);
+ void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int index) =>
+ CopyTo(array, index);
void ICollection.CopyTo(Array array, int index)
{
if (array == null)
+ {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+ }
+
if (array.Rank != 1)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported);
+ }
+
if (array.GetLowerBound(0) != 0)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound);
+ }
+
if ((uint)index > (uint)array.Length)
+ {
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+ }
+
if (array.Length - index < Count)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
+ }
if (array is KeyValuePair<TKey, TValue>[] pairs)
{
}
}
- IEnumerator IEnumerable.GetEnumerator()
- => new Enumerator(this, Enumerator.KeyValuePair);
+ IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this, Enumerator.KeyValuePair);
/// <summary>
/// Ensures that the dictionary can hold up to 'capacity' entries without any further expansion of its backing storage
public int EnsureCapacity(int capacity)
{
if (capacity < 0)
+ {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
+ }
+
int currentCapacity = _entries == null ? 0 : _entries.Length;
if (currentCapacity >= capacity)
+ {
return currentCapacity;
+ }
+
_version++;
+
if (_buckets == null)
+ {
return Initialize(capacity);
+ }
int newSize = HashHelpers.GetPrime(capacity);
Resize(newSize, forceNewHashCodes: false);
/// <summary>
/// Sets the capacity of this dictionary to what it would be if it had been originally initialized with all its entries
- ///
+ /// </summary>
+ /// <remarks>
/// This method can be used to minimize the memory overhead
/// once it is known that no new elements will be added.
///
///
/// dictionary.Clear();
/// dictionary.TrimExcess();
- /// </summary>
- public void TrimExcess()
- => TrimExcess(Count);
+ /// </remarks>
+ public void TrimExcess() => TrimExcess(Count);
/// <summary>
/// Sets the capacity of this dictionary to hold up 'capacity' entries without any further expansion of its backing storage
- ///
+ /// </summary>
+ /// <remarks>
/// This method can be used to minimize the memory overhead
/// once it is known that no new elements will be added.
- /// </summary>
+ /// </remarks>
public void TrimExcess(int capacity)
{
if (capacity < Count)
+ {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
+ }
int newSize = HashHelpers.GetPrime(capacity);
Entry[]? oldEntries = _entries;
int currentCapacity = oldEntries == null ? 0 : oldEntries.Length;
if (newSize >= currentCapacity)
+ {
return;
+ }
int oldCount = _count;
_version++;
ref Entry entry = ref entries![count];
entry = oldEntries[i];
ref int bucket = ref GetBucket(hashCode);
- // Value in _buckets is 1-based
- entry.next = bucket - 1;
- // Value in _buckets is 1-based
+ entry.next = bucket - 1; // Value in _buckets is 1-based
bucket = count + 1;
count++;
}
}
+
_count = count;
_freeCount = 0;
}
bool IDictionary.IsReadOnly => false;
- ICollection IDictionary.Keys => (ICollection)Keys;
+ ICollection IDictionary.Keys => Keys;
- ICollection IDictionary.Values => (ICollection)Values;
+ ICollection IDictionary.Values => Values;
object? IDictionary.this[object key]
{
return value;
}
}
+
return null;
}
set
return false;
}
- IDictionaryEnumerator IDictionary.GetEnumerator()
- => new Enumerator(this, Enumerator.DictEntry);
+ IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this, Enumerator.DictEntry);
void IDictionary.Remove(object key)
{
#endif
}
- public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>,
- IDictionaryEnumerator
+ private struct Entry
+ {
+ public uint hashCode;
+ /// <summary>
+ /// 0-based index of next entry in chain: -1 means end of chain
+ /// also encodes whether this entry _itself_ is part of the free list by changing sign and subtracting 3,
+ /// so -2 means end of free list, -3 means index 0 but on free list, -4 means index 1 but on free list, etc.
+ /// </summary>
+ public int next;
+ public TKey key; // Key of entry
+ public TValue value; // Value of entry
+ }
+
+ public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IDictionaryEnumerator
{
private readonly Dictionary<TKey, TValue> _dictionary;
private readonly int _version;
public KeyValuePair<TKey, TValue> Current => _current;
- public void Dispose()
- {
- }
+ public void Dispose() { }
object? IEnumerator.Current
{
{
return new DictionaryEntry(_current.Key, _current.Value);
}
- else
- {
- return new KeyValuePair<TKey, TValue>(_current.Key, _current.Value);
- }
+
+ return new KeyValuePair<TKey, TValue>(_current.Key, _current.Value);
}
}
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);
}
+
_dictionary = dictionary;
}
- public Enumerator GetEnumerator()
- => new Enumerator(_dictionary);
+ public Enumerator GetEnumerator() => new Enumerator(_dictionary);
public void CopyTo(TKey[] array, int index)
{
bool ICollection<TKey>.IsReadOnly => true;
- void ICollection<TKey>.Add(TKey item)
- => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet);
+ void ICollection<TKey>.Add(TKey item) =>
+ ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet);
- void ICollection<TKey>.Clear()
- => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet);
+ void ICollection<TKey>.Clear() =>
+ ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet);
- bool ICollection<TKey>.Contains(TKey item)
- => _dictionary.ContainsKey(item);
+ bool ICollection<TKey>.Contains(TKey item) =>
+ _dictionary.ContainsKey(item);
bool ICollection<TKey>.Remove(TKey item)
{
return false;
}
- IEnumerator<TKey> IEnumerable<TKey>.GetEnumerator()
- => new Enumerator(_dictionary);
+ IEnumerator<TKey> IEnumerable<TKey>.GetEnumerator() => new Enumerator(_dictionary);
- IEnumerator IEnumerable.GetEnumerator()
- => new Enumerator(_dictionary);
+ IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_dictionary);
void ICollection.CopyTo(Array array, int index)
{
if (array == null)
+ {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+ }
+
if (array.Rank != 1)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported);
+ }
+
if (array.GetLowerBound(0) != 0)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound);
+ }
+
if ((uint)index > (uint)array.Length)
+ {
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+ }
+
if (array.Length - index < _dictionary.Count)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
+ }
if (array is TKey[] keys)
{
_currentKey = default;
}
- public void Dispose()
- {
- }
+ public void Dispose() { }
public bool MoveNext()
{
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);
}
+
_dictionary = dictionary;
}
- public Enumerator GetEnumerator()
- => new Enumerator(_dictionary);
+ public Enumerator GetEnumerator() => new Enumerator(_dictionary);
public void CopyTo(TValue[] array, int index)
{
bool ICollection<TValue>.IsReadOnly => true;
- void ICollection<TValue>.Add(TValue item)
- => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet);
+ void ICollection<TValue>.Add(TValue item) =>
+ ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet);
bool ICollection<TValue>.Remove(TValue item)
{
return false;
}
- void ICollection<TValue>.Clear()
- => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet);
+ void ICollection<TValue>.Clear() =>
+ ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet);
- bool ICollection<TValue>.Contains(TValue item)
- => _dictionary.ContainsValue(item);
+ bool ICollection<TValue>.Contains(TValue item) => _dictionary.ContainsValue(item);
- IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator()
- => new Enumerator(_dictionary);
+ IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => new Enumerator(_dictionary);
- IEnumerator IEnumerable.GetEnumerator()
- => new Enumerator(_dictionary);
+ IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_dictionary);
void ICollection.CopyTo(Array array, int index)
{
if (array == null)
+ {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+ }
+
if (array.Rank != 1)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported);
+ }
+
if (array.GetLowerBound(0) != 0)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound);
+ }
+
if ((uint)index > (uint)array.Length)
+ {
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
+ }
+
if (array.Length - index < _dictionary.Count)
+ {
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
+ }
if (array is TValue[] values)
{
_currentValue = default;
}
- public void Dispose()
- {
- }
+ public void Dispose() { }
public bool MoveNext()
{
{
ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
}
+
_index = 0;
_currentValue = default;
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+
+using Internal.Runtime.CompilerServices;
+
+namespace System.Collections.Generic
+{
+ [DebuggerTypeProxy(typeof(ICollectionDebugView<>))]
+ [DebuggerDisplay("Count = {Count}")]
+ [Serializable]
+ [TypeForwardedFrom("System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
+ public class HashSet<T> : ICollection<T>, ISet<T>, IReadOnlyCollection<T>, IReadOnlySet<T>, ISerializable, IDeserializationCallback
+ {
+ // This uses the same array-based implementation as Dictionary<TKey, TValue>.
+
+ // Constants for serialization
+ private const string CapacityName = "Capacity"; // Do not rename (binary serialization)
+ private const string ElementsName = "Elements"; // Do not rename (binary serialization)
+ private const string ComparerName = "Comparer"; // Do not rename (binary serialization)
+ private const string VersionName = "Version"; // Do not rename (binary serialization)
+
+ /// <summary>Cutoff point for stackallocs. This corresponds to the number of ints.</summary>
+ private const int StackAllocThreshold = 100;
+
+ /// <summary>
+ /// When constructing a hashset from an existing collection, it may contain duplicates,
+ /// so this is used as the max acceptable excess ratio of capacity to count. Note that
+ /// this is only used on the ctor and not to automatically shrink if the hashset has, e.g,
+ /// a lot of adds followed by removes. Users must explicitly shrink by calling TrimExcess.
+ /// This is set to 3 because capacity is acceptable as 2x rounded up to nearest prime.
+ /// </summary>
+ private const int ShrinkThreshold = 3;
+ private const int StartOfFreeList = -3;
+
+ private int[]? _buckets;
+ private Entry[]? _entries;
+#if TARGET_64BIT
+ private ulong _fastModMultiplier;
+#endif
+ private int _count;
+ private int _freeList;
+ private int _freeCount;
+ private int _version;
+ private IEqualityComparer<T>? _comparer;
+ private SerializationInfo? _siInfo; // temporary variable needed during deserialization
+
+ #region Constructors
+
+ public HashSet() : this((IEqualityComparer<T>?)null) { }
+
+ public HashSet(IEqualityComparer<T>? comparer)
+ {
+ if (comparer != null && comparer != EqualityComparer<T>.Default) // first check for null to avoid forcing default comparer instantiation unnecessarily
+ {
+ _comparer = comparer;
+ }
+
+ if (typeof(T) == typeof(string) && _comparer == null)
+ {
+ // To start, move off default comparer for string which is randomized.
+ _comparer = (IEqualityComparer<T>)NonRandomizedStringEqualityComparer.Default;
+ }
+ }
+
+ public HashSet(int capacity) : this(capacity, null) { }
+
+ public HashSet(IEnumerable<T> collection) : this(collection, null) { }
+
+ public HashSet(IEnumerable<T> collection, IEqualityComparer<T>? comparer) : this(comparer)
+ {
+ if (collection == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
+ }
+
+ if (collection is HashSet<T> otherAsHashSet && EqualityComparersAreEqual(this, otherAsHashSet))
+ {
+ ConstructFrom(otherAsHashSet);
+ }
+ else
+ {
+ // To avoid excess resizes, first set size based on collection's count. The collection may
+ // contain duplicates, so call TrimExcess if resulting HashSet is larger than the threshold.
+ if (collection is ICollection<T> coll)
+ {
+ int count = coll.Count;
+ if (count > 0)
+ {
+ Initialize(count);
+ }
+ }
+
+ UnionWith(collection);
+
+ if (_count > 0 && _entries!.Length / _count > ShrinkThreshold)
+ {
+ TrimExcess();
+ }
+ }
+ }
+
+ public HashSet(int capacity, IEqualityComparer<T>? comparer) : this(comparer)
+ {
+ if (capacity < 0)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
+ }
+
+ if (capacity > 0)
+ {
+ Initialize(capacity);
+ }
+ }
+
+ protected HashSet(SerializationInfo info, StreamingContext context)
+ {
+ // We can't do anything with the keys and values until the entire graph has been
+ // deserialized and we have a reasonable estimate that GetHashCode is not going to
+ // fail. For the time being, we'll just cache this. The graph is not valid until
+ // OnDeserialization has been called.
+ _siInfo = info;
+ }
+
+ /// <summary>Initializes the HashSet from another HashSet with the same element type and equality comparer.</summary>
+ private void ConstructFrom(HashSet<T> source)
+ {
+ if (source.Count == 0)
+ {
+ // As well as short-circuiting on the rest of the work done,
+ // this avoids errors from trying to access source._buckets
+ // or source._entries when they aren't initialized.
+ return;
+ }
+
+ int capacity = source._buckets!.Length;
+ int threshold = HashHelpers.ExpandPrime(source.Count + 1);
+
+ if (threshold >= capacity)
+ {
+ _buckets = (int[])source._buckets.Clone();
+ _entries = (Entry[])source._entries!.Clone();
+ _freeList = source._freeList;
+ _freeCount = source._freeCount;
+ _count = source._count;
+#if TARGET_64BIT
+ _fastModMultiplier = source._fastModMultiplier;
+#endif
+ }
+ else
+ {
+ Initialize(source.Count);
+
+ Entry[]? entries = source._entries;
+ for (int i = 0; i < source._count; i++)
+ {
+ ref Entry entry = ref entries![i];
+ if (entry.Next >= -1)
+ {
+ AddIfNotPresent(entry.Value, out _);
+ }
+ }
+ }
+
+ Debug.Assert(Count == source.Count);
+ }
+
+ #endregion
+
+ #region ICollection<T> methods
+
+ void ICollection<T>.Add(T item) => AddIfNotPresent(item, out _);
+
+ /// <summary>Removes all elements from the <see cref="HashSet{T}"/> object.</summary>
+ public void Clear()
+ {
+ int count = _count;
+ if (count > 0)
+ {
+ Debug.Assert(_buckets != null, "_buckets should be non-null");
+ Debug.Assert(_entries != null, "_entries should be non-null");
+
+ Array.Clear(_buckets, 0, _buckets.Length);
+ _count = 0;
+ _freeList = -1;
+ _freeCount = 0;
+ Array.Clear(_entries, 0, count);
+ }
+ }
+
+ /// <summary>Determines whether the <see cref="HashSet{T}"/> contains the specified element.</summary>
+ /// <param name="item">The element to locate in the <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object contains the specified element; otherwise, false.</returns>
+ public bool Contains(T item) => FindItemIndex(item) >= 0;
+
+ /// <summary>Gets the index of the item in <see cref="_entries"/>, or -1 if it's not in the set.</summary>
+ private int FindItemIndex(T item)
+ {
+ int[]? buckets = _buckets;
+ if (buckets != null)
+ {
+ Entry[]? entries = _entries;
+ Debug.Assert(entries != null, "Expected _entries to be initialized");
+
+ uint collisionCount = 0;
+ IEqualityComparer<T>? comparer = _comparer;
+
+ if (comparer == null)
+ {
+ int hashCode = item != null ? item.GetHashCode() : 0;
+ if (typeof(T).IsValueType)
+ {
+ // ValueType: Devirtualize with EqualityComparer<TValue>.Default intrinsic
+ int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.HashCode == hashCode && EqualityComparer<T>.Default.Equals(entry.Value, item))
+ {
+ return i;
+ }
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop, which means a concurrent update has happened.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+ else
+ {
+ // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize (https://github.com/dotnet/runtime/issues/10050),
+ // so cache in a local rather than get EqualityComparer per loop iteration.
+ EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
+ int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.HashCode == hashCode && defaultComparer.Equals(entry.Value, item))
+ {
+ return i;
+ }
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop, which means a concurrent update has happened.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+ }
+ else
+ {
+ int hashCode = item != null ? comparer.GetHashCode(item) : 0;
+ int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.HashCode == hashCode && comparer.Equals(entry.Value, item))
+ {
+ return i;
+ }
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop, which means a concurrent update has happened.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /// <summary>Gets a reference to the specified hashcode's bucket, containing an index into <see cref="_entries"/>.</summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private ref int GetBucketRef(int hashCode)
+ {
+ int[] buckets = _buckets!;
+#if TARGET_64BIT
+ return ref buckets[HashHelpers.FastMod((uint)hashCode, (uint)buckets.Length, _fastModMultiplier)];
+#else
+ return ref buckets[(uint)hashCode % (uint)buckets.Length];
+#endif
+ }
+
+ public bool Remove(T item)
+ {
+ if (_buckets != null)
+ {
+ Entry[]? entries = _entries;
+ Debug.Assert(entries != null, "entries should be non-null");
+
+ uint collisionCount = 0;
+ int last = -1;
+ int hashCode = item != null ? (_comparer?.GetHashCode(item) ?? item.GetHashCode()) : 0;
+
+ ref int bucket = ref GetBucketRef(hashCode);
+ int i = bucket - 1; // Value in buckets is 1-based
+
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+
+ if (entry.HashCode == hashCode && (_comparer?.Equals(entry.Value, item) ?? EqualityComparer<T>.Default.Equals(entry.Value, item)))
+ {
+ if (last < 0)
+ {
+ bucket = entry.Next + 1; // Value in buckets is 1-based
+ }
+ else
+ {
+ entries[last].Next = entry.Next;
+ }
+
+ Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646");
+ entry.Next = StartOfFreeList - _freeList;
+
+ if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
+ {
+ entry.Value = default!;
+ }
+
+ _freeList = i;
+ _freeCount++;
+ return true;
+ }
+
+ last = i;
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop; which means a concurrent update has happened.
+ // Break out of the loop and throw, rather than looping forever.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>Gets the number of elements that are contained in the set.</summary>
+ public int Count => _count - _freeCount;
+
+ bool ICollection<T>.IsReadOnly => false;
+
+ #endregion
+
+ #region IEnumerable methods
+
+ public Enumerator GetEnumerator() => new Enumerator(this);
+
+ IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ #endregion
+
+ #region ISerializable methods
+
+ public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ if (info == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.info);
+ }
+
+ info.AddValue(VersionName, _version); // need to serialize version to avoid problems with serializing while enumerating
+ info.AddValue(ComparerName, _comparer ?? EqualityComparer<T>.Default, typeof(IEqualityComparer<T>));
+ info.AddValue(CapacityName, _buckets == null ? 0 : _buckets.Length);
+
+ if (_buckets != null)
+ {
+ var array = new T[Count];
+ CopyTo(array);
+ info.AddValue(ElementsName, array, typeof(T[]));
+ }
+ }
+
+ #endregion
+
+ #region IDeserializationCallback methods
+
+ public virtual void OnDeserialization(object? sender)
+ {
+ if (_siInfo == null)
+ {
+ // It might be necessary to call OnDeserialization from a container if the
+ // container object also implements OnDeserialization. We can return immediately
+ // if this function is called twice. Note we set _siInfo to null at the end of this method.
+ return;
+ }
+
+ int capacity = _siInfo.GetInt32(CapacityName);
+ _comparer = (IEqualityComparer<T>)_siInfo.GetValue(ComparerName, typeof(IEqualityComparer<T>))!;
+ _freeList = -1;
+ _freeCount = 0;
+
+ if (capacity != 0)
+ {
+ _buckets = new int[capacity];
+ _entries = new Entry[capacity];
+#if TARGET_64BIT
+ _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)capacity);
+#endif
+
+ T[]? array = (T[]?)_siInfo.GetValue(ElementsName, typeof(T[]));
+ if (array == null)
+ {
+ ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MissingKeys);
+ }
+
+ // There are no resizes here because we already set capacity above.
+ for (int i = 0; i < array.Length; i++)
+ {
+ AddIfNotPresent(array[i], out _);
+ }
+ }
+ else
+ {
+ _buckets = null;
+ }
+
+ _version = _siInfo.GetInt32(VersionName);
+ _siInfo = null;
+ }
+
+ #endregion
+
+ #region HashSet methods
+
+ /// <summary>Adds the specified element to the <see cref="HashSet{T}"/>.</summary>
+ /// <param name="item">The element to add to the set.</param>
+ /// <returns>true if the element is added to the <see cref="HashSet{T}"/> object; false if the element is already present.</returns>
+ public bool Add(T item) => AddIfNotPresent(item, out _);
+
+ /// <summary>Searches the set for a given value and returns the equal value it finds, if any.</summary>
+ /// <param name="equalValue">The value to search for.</param>
+ /// <param name="actualValue">The value from the set that the search found, or the default value of <typeparamref name="T"/> when the search yielded no match.</param>
+ /// <returns>A value indicating whether the search was successful.</returns>
+ /// <remarks>
+ /// This can be useful when you want to reuse a previously stored reference instead of
+ /// a newly constructed one (so that more sharing of references can occur) or to look up
+ /// a value that has more complete data than the value you currently have, although their
+ /// comparer functions indicate they are equal.
+ /// </remarks>
+ public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue)
+ {
+ if (_buckets != null)
+ {
+ int index = FindItemIndex(equalValue);
+ if (index >= 0)
+ {
+ actualValue = _entries![index].Value;
+ return true;
+ }
+ }
+
+ actualValue = default;
+ return false;
+ }
+
+ /// <summary>Modifies the current <see cref="HashSet{T}"/> object to contain all elements that are present in itself, the specified collection, or both.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ public void UnionWith(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ foreach (T item in other)
+ {
+ AddIfNotPresent(item, out _);
+ }
+ }
+
+ /// <summary>Modifies the current <see cref="HashSet{T}"/> object to contain only elements that are present in that object and in the specified collection.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ public void IntersectWith(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // Intersection of anything with empty set is empty set, so return if count is 0.
+ // Same if the set intersecting with itself is the same set.
+ if (Count == 0 || other == this)
+ {
+ return;
+ }
+
+ // If other is known to be empty, intersection is empty set; remove all elements, and we're done.
+ if (other is ICollection<T> otherAsCollection)
+ {
+ if (otherAsCollection.Count == 0)
+ {
+ Clear();
+ return;
+ }
+
+ // Faster if other is a hashset using same equality comparer; so check
+ // that other is a hashset using the same equality comparer.
+ if (other is HashSet<T> otherAsSet && EqualityComparersAreEqual(this, otherAsSet))
+ {
+ IntersectWithHashSetWithSameComparer(otherAsSet);
+ return;
+ }
+ }
+
+ IntersectWithEnumerable(other);
+ }
+
+ /// <summary>Removes all elements in the specified collection from the current <see cref="HashSet{T}"/> object.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ public void ExceptWith(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // This is already the empty set; return.
+ if (Count == 0)
+ {
+ return;
+ }
+
+ // Special case if other is this; a set minus itself is the empty set.
+ if (other == this)
+ {
+ Clear();
+ return;
+ }
+
+ // Remove every element in other from this.
+ foreach (T element in other)
+ {
+ Remove(element);
+ }
+ }
+
+ /// <summary>Modifies the current <see cref="HashSet{T}"/> object to contain only elements that are present either in that object or in the specified collection, but not both.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ public void SymmetricExceptWith(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // If set is empty, then symmetric difference is other.
+ if (Count == 0)
+ {
+ UnionWith(other);
+ return;
+ }
+
+ // Special-case this; the symmetric difference of a set with itself is the empty set.
+ if (other == this)
+ {
+ Clear();
+ return;
+ }
+
+ // If other is a HashSet, it has unique elements according to its equality comparer,
+ // but if they're using different equality comparers, then assumption of uniqueness
+ // will fail. So first check if other is a hashset using the same equality comparer;
+ // symmetric except is a lot faster and avoids bit array allocations if we can assume
+ // uniqueness.
+ if (other is HashSet<T> otherAsSet && EqualityComparersAreEqual(this, otherAsSet))
+ {
+ SymmetricExceptWithUniqueHashSet(otherAsSet);
+ }
+ else
+ {
+ SymmetricExceptWithEnumerable(other);
+ }
+ }
+
+ /// <summary>Determines whether a <see cref="HashSet{T}"/> object is a subset of the specified collection.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object is a subset of <paramref name="other"/>; otherwise, false.</returns>
+ public bool IsSubsetOf(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // The empty set is a subset of any set, and a set is a subset of itself.
+ // Set is always a subset of itself
+ if (Count == 0 || other == this)
+ {
+ return true;
+ }
+
+ // Faster if other has unique elements according to this equality comparer; so check
+ // that other is a hashset using the same equality comparer.
+ if (other is HashSet<T> otherAsSet && EqualityComparersAreEqual(this, otherAsSet))
+ {
+ // if this has more elements then it can't be a subset
+ if (Count > otherAsSet.Count)
+ {
+ return false;
+ }
+
+ // already checked that we're using same equality comparer. simply check that
+ // each element in this is contained in other.
+ return IsSubsetOfHashSetWithSameComparer(otherAsSet);
+ }
+
+ (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: false);
+ return uniqueCount == Count && unfoundCount >= 0;
+ }
+
+ /// <summary>Determines whether a <see cref="HashSet{T}"/> object is a proper subset of the specified collection.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object is a proper subset of <paramref name="other"/>; otherwise, false.</returns>
+ public bool IsProperSubsetOf(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // No set is a proper subset of itself.
+ if (other == this)
+ {
+ return false;
+ }
+
+ if (other is ICollection<T> otherAsCollection)
+ {
+ // No set is a proper subset of an empty set.
+ if (otherAsCollection.Count == 0)
+ {
+ return false;
+ }
+
+ // The empty set is a proper subset of anything but the empty set.
+ if (Count == 0)
+ {
+ return otherAsCollection.Count > 0;
+ }
+
+ // Faster if other is a hashset (and we're using same equality comparer).
+ if (other is HashSet<T> otherAsSet && EqualityComparersAreEqual(this, otherAsSet))
+ {
+ if (Count >= otherAsSet.Count)
+ {
+ return false;
+ }
+
+ // This has strictly less than number of items in other, so the following
+ // check suffices for proper subset.
+ return IsSubsetOfHashSetWithSameComparer(otherAsSet);
+ }
+ }
+
+ (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: false);
+ return uniqueCount == Count && unfoundCount > 0;
+ }
+
+ /// <summary>Determines whether a <see cref="HashSet{T}"/> object is a proper superset of the specified collection.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object is a superset of <paramref name="other"/>; otherwise, false.</returns>
+ public bool IsSupersetOf(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // A set is always a superset of itself.
+ if (other == this)
+ {
+ return true;
+ }
+
+ // Try to fall out early based on counts.
+ if (other is ICollection<T> otherAsCollection)
+ {
+ // If other is the empty set then this is a superset.
+ if (otherAsCollection.Count == 0)
+ {
+ return true;
+ }
+
+ // Try to compare based on counts alone if other is a hashset with same equality comparer.
+ if (other is HashSet<T> otherAsSet &&
+ EqualityComparersAreEqual(this, otherAsSet) &&
+ otherAsSet.Count > Count)
+ {
+ return false;
+ }
+ }
+
+ return ContainsAllElements(other);
+ }
+
+ /// <summary>Determines whether a <see cref="HashSet{T}"/> object is a proper superset of the specified collection.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object is a proper superset of <paramref name="other"/>; otherwise, false.</returns>
+ public bool IsProperSupersetOf(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // The empty set isn't a proper superset of any set, and a set is never a strict superset of itself.
+ if (Count == 0 || other == this)
+ {
+ return false;
+ }
+
+ if (other is ICollection<T> otherAsCollection)
+ {
+ // If other is the empty set then this is a superset.
+ if (otherAsCollection.Count == 0)
+ {
+ // Note that this has at least one element, based on above check.
+ return true;
+ }
+
+ // Faster if other is a hashset with the same equality comparer
+ if (other is HashSet<T> otherAsSet && EqualityComparersAreEqual(this, otherAsSet))
+ {
+ if (otherAsSet.Count >= Count)
+ {
+ return false;
+ }
+
+ // Now perform element check.
+ return ContainsAllElements(otherAsSet);
+ }
+ }
+
+ // Couldn't fall out in the above cases; do it the long way
+ (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: true);
+ return uniqueCount < Count && unfoundCount == 0;
+ }
+
+ /// <summary>Determines whether the current <see cref="HashSet{T}"/> object and a specified collection share common elements.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object and <paramref name="other"/> share at least one common element; otherwise, false.</returns>
+ public bool Overlaps(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ if (Count == 0)
+ {
+ return false;
+ }
+
+ // Set overlaps itself
+ if (other == this)
+ {
+ return true;
+ }
+
+ foreach (T element in other)
+ {
+ if (Contains(element))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>Determines whether a <see cref="HashSet{T}"/> object and the specified collection contain the same elements.</summary>
+ /// <param name="other">The collection to compare to the current <see cref="HashSet{T}"/> object.</param>
+ /// <returns>true if the <see cref="HashSet{T}"/> object is equal to <paramref name="other"/>; otherwise, false.</returns>
+ public bool SetEquals(IEnumerable<T> other)
+ {
+ if (other == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.other);
+ }
+
+ // A set is equal to itself.
+ if (other == this)
+ {
+ return true;
+ }
+
+ // Faster if other is a hashset and we're using same equality comparer.
+ if (other is HashSet<T> otherAsSet && EqualityComparersAreEqual(this, otherAsSet))
+ {
+ // Attempt to return early: since both contain unique elements, if they have
+ // different counts, then they can't be equal.
+ if (Count != otherAsSet.Count)
+ {
+ return false;
+ }
+
+ // Already confirmed that the sets have the same number of distinct elements, so if
+ // one is a superset of the other then they must be equal.
+ return ContainsAllElements(otherAsSet);
+ }
+ else
+ {
+ // If this count is 0 but other contains at least one element, they can't be equal.
+ if (Count == 0 &&
+ other is ICollection<T> otherAsCollection &&
+ otherAsCollection.Count > 0)
+ {
+ return false;
+ }
+
+ (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: true);
+ return uniqueCount == Count && unfoundCount == 0;
+ }
+ }
+
+ public void CopyTo(T[] array) => CopyTo(array, 0, Count);
+
+ /// <summary>Copies the elements of a <see cref="HashSet{T}"/> object to an array, starting at the specified array index.</summary>
+ /// <param name="array">The destination array.</param>
+ /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
+ public void CopyTo(T[] array, int arrayIndex) => CopyTo(array, arrayIndex, Count);
+
+ public void CopyTo(T[] array, int arrayIndex, int count)
+ {
+ if (array == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
+ }
+
+ // Check array index valid index into array.
+ if (arrayIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, SR.ArgumentOutOfRange_NeedNonNegNum);
+ }
+
+ // Also throw if count less than 0.
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, SR.ArgumentOutOfRange_NeedNonNegNum);
+ }
+
+ // Will the array, starting at arrayIndex, be able to hold elements? Note: not
+ // checking arrayIndex >= array.Length (consistency with list of allowing
+ // count of 0; subsequent check takes care of the rest)
+ if (arrayIndex > array.Length || count > array.Length - arrayIndex)
+ {
+ ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
+ }
+
+ Entry[]? entries = _entries;
+ for (int i = 0; i < _count && count != 0; i++)
+ {
+ ref Entry entry = ref entries![i];
+ if (entry.Next >= -1)
+ {
+ array[arrayIndex++] = entry.Value;
+ count--;
+ }
+ }
+ }
+
+ /// <summary>Removes all elements that match the conditions defined by the specified predicate from a <see cref="HashSet{T}"/> collection.</summary>
+ public int RemoveWhere(Predicate<T> match)
+ {
+ if (match == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
+ }
+
+ Entry[]? entries = _entries;
+ int numRemoved = 0;
+ for (int i = 0; i < _count; i++)
+ {
+ ref Entry entry = ref entries![i];
+ if (entry.Next >= -1)
+ {
+ // Cache value in case delegate removes it
+ T value = entry.Value;
+ if (match(value))
+ {
+ // Check again that remove actually removed it.
+ if (Remove(value))
+ {
+ numRemoved++;
+ }
+ }
+ }
+ }
+
+ return numRemoved;
+ }
+
+ /// <summary>Gets the <see cref="IEqualityComparer"/> object that is used to determine equality for the values in the set.</summary>
+ public IEqualityComparer<T> Comparer =>
+ (_comparer == null || _comparer is NonRandomizedStringEqualityComparer) ?
+ EqualityComparer<T>.Default :
+ _comparer;
+
+ /// <summary>Ensures that this hash set can hold the specified number of elements without growing.</summary>
+ public int EnsureCapacity(int capacity)
+ {
+ if (capacity < 0)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
+ }
+
+ int currentCapacity = _entries == null ? 0 : _entries.Length;
+ if (currentCapacity >= capacity)
+ {
+ return currentCapacity;
+ }
+
+ if (_buckets == null)
+ {
+ return Initialize(capacity);
+ }
+
+ int newSize = HashHelpers.GetPrime(capacity);
+ Resize(newSize, forceNewHashCodes: false);
+ return newSize;
+ }
+
+ private void Resize() => Resize(HashHelpers.ExpandPrime(_count), forceNewHashCodes: false);
+
+ private void Resize(int newSize, bool forceNewHashCodes)
+ {
+ // Value types never rehash
+ Debug.Assert(!forceNewHashCodes || !typeof(T).IsValueType);
+ Debug.Assert(_entries != null, "_entries should be non-null");
+ Debug.Assert(newSize >= _entries.Length);
+
+ var entries = new Entry[newSize];
+
+ int count = _count;
+ Array.Copy(_entries, entries, count);
+
+ if (!typeof(T).IsValueType && forceNewHashCodes)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.Next >= -1)
+ {
+ Debug.Assert(_comparer == null);
+ entry.HashCode = entry.Value != null ? entry.Value!.GetHashCode() : 0;
+ }
+ }
+ }
+
+ // Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
+ _buckets = new int[newSize];
+#if TARGET_64BIT
+ _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize);
+#endif
+ for (int i = 0; i < count; i++)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.Next >= -1)
+ {
+ ref int bucket = ref GetBucketRef(entry.HashCode);
+ entry.Next = bucket - 1; // Value in _buckets is 1-based
+ bucket = i + 1;
+ }
+ }
+
+ _entries = entries;
+ }
+
+ /// <summary>
+ /// Sets the capacity of a <see cref="HashSet{T}"/> object to the actual number of elements it contains,
+ /// rounded up to a nearby, implementation-specific value.
+ /// </summary>
+ public void TrimExcess()
+ {
+ int capacity = Count;
+
+ int newSize = HashHelpers.GetPrime(capacity);
+ Entry[]? oldEntries = _entries;
+ int currentCapacity = oldEntries == null ? 0 : oldEntries.Length;
+ if (newSize >= currentCapacity)
+ {
+ return;
+ }
+
+ int oldCount = _count;
+ _version++;
+ Initialize(newSize);
+ Entry[]? entries = _entries;
+ int count = 0;
+ for (int i = 0; i < oldCount; i++)
+ {
+ int hashCode = oldEntries![i].HashCode; // At this point, we know we have entries.
+ if (oldEntries[i].Next >= -1)
+ {
+ ref Entry entry = ref entries![count];
+ entry = oldEntries[i];
+ ref int bucket = ref GetBucketRef(hashCode);
+ entry.Next = bucket - 1; // Value in _buckets is 1-based
+ bucket = count + 1;
+ count++;
+ }
+ }
+
+ _count = capacity;
+ _freeCount = 0;
+ }
+
+ #endregion
+
+ #region Helper methods
+
+ /// <summary>Returns an <see cref="IEqualityComparer"/> object that can be used for equality testing of a <see cref="HashSet{T}"/> object.</summary>
+ public static IEqualityComparer<HashSet<T>> CreateSetComparer() => new HashSetEqualityComparer<T>();
+
+ /// <summary>
+ /// Initializes buckets and slots arrays. Uses suggested capacity by finding next prime
+ /// greater than or equal to capacity.
+ /// </summary>
+ private int Initialize(int capacity)
+ {
+ int size = HashHelpers.GetPrime(capacity);
+ var buckets = new int[size];
+ var entries = new Entry[size];
+
+ // Assign member variables after both arrays are allocated to guard against corruption from OOM if second fails.
+ _freeList = -1;
+ _buckets = buckets;
+ _entries = entries;
+#if TARGET_64BIT
+ _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)size);
+#endif
+
+ return size;
+ }
+
+ /// <summary>Adds the specified element to the set if it's not already contained.</summary>
+ /// <param name="value">The element to add to the set.</param>
+ /// <param name="location">The index into <see cref="_entries"/> of the element.</param>
+ /// <returns>true if the element is added to the <see cref="HashSet{T}"/> object; false if the element is already present.</returns>
+ private bool AddIfNotPresent(T value, out int location)
+ {
+ if (_buckets == null)
+ {
+ Initialize(0);
+ }
+ Debug.Assert(_buckets != null);
+
+ Entry[]? entries = _entries;
+ Debug.Assert(entries != null, "expected entries to be non-null");
+
+ IEqualityComparer<T>? comparer = _comparer;
+ int hashCode;
+
+ uint collisionCount = 0;
+ ref int bucket = ref Unsafe.NullRef<int>();
+
+ if (comparer == null)
+ {
+ hashCode = value != null ? value.GetHashCode() : 0;
+ bucket = ref GetBucketRef(hashCode);
+ int i = bucket - 1; // Value in _buckets is 1-based
+ if (typeof(T).IsValueType)
+ {
+ // ValueType: Devirtualize with EqualityComparer<TValue>.Default intrinsic
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.HashCode == hashCode && EqualityComparer<T>.Default.Equals(entry.Value, value))
+ {
+ location = i;
+ return false;
+ }
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop, which means a concurrent update has happened.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+ else
+ {
+ // Object type: Shared Generic, EqualityComparer<TValue>.Default won't devirtualize (https://github.com/dotnet/runtime/issues/10050),
+ // so cache in a local rather than get EqualityComparer per loop iteration.
+ EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.HashCode == hashCode && defaultComparer.Equals(entry.Value, value))
+ {
+ location = i;
+ return false;
+ }
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop, which means a concurrent update has happened.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+ }
+ else
+ {
+ hashCode = value != null ? comparer.GetHashCode(value) : 0;
+ bucket = ref GetBucketRef(hashCode);
+ int i = bucket - 1; // Value in _buckets is 1-based
+ while (i >= 0)
+ {
+ ref Entry entry = ref entries[i];
+ if (entry.HashCode == hashCode && comparer.Equals(entry.Value, value))
+ {
+ location = i;
+ return false;
+ }
+ i = entry.Next;
+
+ collisionCount++;
+ if (collisionCount > (uint)entries.Length)
+ {
+ // The chain of entries forms a loop, which means a concurrent update has happened.
+ ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
+ }
+ }
+ }
+
+ int index;
+ if (_freeCount > 0)
+ {
+ index = _freeList;
+ _freeCount--;
+ Debug.Assert((StartOfFreeList - entries![_freeList].Next) >= -1, "shouldn't overflow because `next` cannot underflow");
+ _freeList = StartOfFreeList - entries[_freeList].Next;
+ }
+ else
+ {
+ int count = _count;
+ if (count == entries.Length)
+ {
+ Resize();
+ bucket = ref GetBucketRef(hashCode);
+ }
+ index = count;
+ _count = count + 1;
+ entries = _entries;
+ }
+
+ {
+ ref Entry entry = ref entries![index];
+ entry.HashCode = hashCode;
+ entry.Next = bucket - 1; // Value in _buckets is 1-based
+ entry.Value = value;
+ bucket = index + 1;
+ _version++;
+ location = index;
+ }
+
+ // Value types never rehash
+ if (!typeof(T).IsValueType && collisionCount > HashHelpers.HashCollisionThreshold && comparer is NonRandomizedStringEqualityComparer)
+ {
+ // If we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
+ // i.e. EqualityComparer<string>.Default.
+ _comparer = null;
+ Resize(entries.Length, forceNewHashCodes: true);
+ location = FindItemIndex(value);
+ Debug.Assert(location >= 0);
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Checks if this contains of other's elements. Iterates over other's elements and
+ /// returns false as soon as it finds an element in other that's not in this.
+ /// Used by SupersetOf, ProperSupersetOf, and SetEquals.
+ /// </summary>
+ private bool ContainsAllElements(IEnumerable<T> other)
+ {
+ foreach (T element in other)
+ {
+ if (!Contains(element))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Implementation Notes:
+ /// If other is a hashset and is using same equality comparer, then checking subset is
+ /// faster. Simply check that each element in this is in other.
+ ///
+ /// Note: if other doesn't use same equality comparer, then Contains check is invalid,
+ /// which is why callers must take are of this.
+ ///
+ /// If callers are concerned about whether this is a proper subset, they take care of that.
+ /// </summary>
+ internal bool IsSubsetOfHashSetWithSameComparer(HashSet<T> other)
+ {
+ foreach (T item in this)
+ {
+ if (!other.Contains(item))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// If other is a hashset that uses same equality comparer, intersect is much faster
+ /// because we can use other's Contains
+ /// </summary>
+ private void IntersectWithHashSetWithSameComparer(HashSet<T> other)
+ {
+ Entry[]? entries = _entries;
+ for (int i = 0; i < _count; i++)
+ {
+ ref Entry entry = ref entries![i];
+ if (entry.Next >= -1)
+ {
+ T item = entry.Value;
+ if (!other.Contains(item))
+ {
+ Remove(item);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Iterate over other. If contained in this, mark an element in bit array corresponding to
+ /// its position in _slots. If anything is unmarked (in bit array), remove it.
+ ///
+ /// This attempts to allocate on the stack, if below StackAllocThreshold.
+ /// </summary>
+ private unsafe void IntersectWithEnumerable(IEnumerable<T> other)
+ {
+ Debug.Assert(_buckets != null, "_buckets shouldn't be null; callers should check first");
+
+ // Keep track of current last index; don't want to move past the end of our bit array
+ // (could happen if another thread is modifying the collection).
+ int originalCount = _count;
+ int intArrayLength = BitHelper.ToIntArrayLength(originalCount);
+
+ Span<int> span = stackalloc int[StackAllocThreshold];
+ BitHelper bitHelper = intArrayLength <= StackAllocThreshold ?
+ new BitHelper(span.Slice(0, intArrayLength), clear: true) :
+ new BitHelper(new int[intArrayLength], clear: false);
+
+ // Mark if contains: find index of in slots array and mark corresponding element in bit array.
+ foreach (T item in other)
+ {
+ int index = FindItemIndex(item);
+ if (index >= 0)
+ {
+ bitHelper.MarkBit(index);
+ }
+ }
+
+ // If anything unmarked, remove it. Perf can be optimized here if BitHelper had a
+ // FindFirstUnmarked method.
+ for (int i = 0; i < originalCount; i++)
+ {
+ ref Entry entry = ref _entries![i];
+ if (entry.Next >= -1 && !bitHelper.IsMarked(i))
+ {
+ Remove(entry.Value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// if other is a set, we can assume it doesn't have duplicate elements, so use this
+ /// technique: if can't remove, then it wasn't present in this set, so add.
+ ///
+ /// As with other methods, callers take care of ensuring that other is a hashset using the
+ /// same equality comparer.
+ /// </summary>
+ /// <param name="other"></param>
+ private void SymmetricExceptWithUniqueHashSet(HashSet<T> other)
+ {
+ foreach (T item in other)
+ {
+ if (!Remove(item))
+ {
+ AddIfNotPresent(item, out _);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Implementation notes:
+ ///
+ /// Used for symmetric except when other isn't a HashSet. This is more tedious because
+ /// other may contain duplicates. HashSet technique could fail in these situations:
+ /// 1. Other has a duplicate that's not in this: HashSet technique would add then
+ /// remove it.
+ /// 2. Other has a duplicate that's in this: HashSet technique would remove then add it
+ /// back.
+ /// In general, its presence would be toggled each time it appears in other.
+ ///
+ /// This technique uses bit marking to indicate whether to add/remove the item. If already
+ /// present in collection, it will get marked for deletion. If added from other, it will
+ /// get marked as something not to remove.
+ ///
+ /// </summary>
+ /// <param name="other"></param>
+ private unsafe void SymmetricExceptWithEnumerable(IEnumerable<T> other)
+ {
+ int originalCount = _count;
+ int intArrayLength = BitHelper.ToIntArrayLength(originalCount);
+
+ Span<int> itemsToRemoveSpan = stackalloc int[StackAllocThreshold / 2];
+ BitHelper itemsToRemove = intArrayLength <= StackAllocThreshold / 2 ?
+ new BitHelper(itemsToRemoveSpan.Slice(0, intArrayLength), clear: true) :
+ new BitHelper(new int[intArrayLength], clear: false);
+
+ Span<int> itemsAddedFromOtherSpan = stackalloc int[StackAllocThreshold / 2];
+ BitHelper itemsAddedFromOther = intArrayLength <= StackAllocThreshold / 2 ?
+ new BitHelper(itemsAddedFromOtherSpan.Slice(0, intArrayLength), clear: true) :
+ new BitHelper(new int[intArrayLength], clear: false);
+
+ foreach (T item in other)
+ {
+ int location;
+ if (AddIfNotPresent(item, out location))
+ {
+ // wasn't already present in collection; flag it as something not to remove
+ // *NOTE* if location is out of range, we should ignore. BitHelper will
+ // detect that it's out of bounds and not try to mark it. But it's
+ // expected that location could be out of bounds because adding the item
+ // will increase _lastIndex as soon as all the free spots are filled.
+ itemsAddedFromOther.MarkBit(location);
+ }
+ else
+ {
+ // already there...if not added from other, mark for remove.
+ // *NOTE* Even though BitHelper will check that location is in range, we want
+ // to check here. There's no point in checking items beyond originalCount
+ // because they could not have been in the original collection
+ if (location < originalCount && !itemsAddedFromOther.IsMarked(location))
+ {
+ itemsToRemove.MarkBit(location);
+ }
+ }
+ }
+
+ // if anything marked, remove it
+ for (int i = 0; i < originalCount; i++)
+ {
+ if (itemsToRemove.IsMarked(i))
+ {
+ Remove(_entries![i].Value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Determines counts that can be used to determine equality, subset, and superset. This
+ /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet
+ /// these properties can be checked faster without use of marking because we can assume
+ /// other has no duplicates.
+ ///
+ /// The following count checks are performed by callers:
+ /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything
+ /// in other is in this and everything in this is in other
+ /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may
+ /// have elements not in this and everything in this is in other
+ /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e
+ /// other must have at least one element not in this and everything in this is in other
+ /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less
+ /// than _count; i.e. everything in other was in this and this had at least one element
+ /// not contained in other.
+ ///
+ /// An earlier implementation used delegates to perform these checks rather than returning
+ /// an ElementCount struct; however this was changed due to the perf overhead of delegates.
+ /// </summary>
+ /// <param name="other"></param>
+ /// <param name="returnIfUnfound">Allows us to finish faster for equals and proper superset
+ /// because unfoundCount must be 0.</param>
+ private unsafe (int UniqueCount, int UnfoundCount) CheckUniqueAndUnfoundElements(IEnumerable<T> other, bool returnIfUnfound)
+ {
+ // Need special case in case this has no elements.
+ if (_count == 0)
+ {
+ int numElementsInOther = 0;
+ foreach (T item in other)
+ {
+ numElementsInOther++;
+ break; // break right away, all we want to know is whether other has 0 or 1 elements
+ }
+
+ return (UniqueCount: 0, UnfoundCount: numElementsInOther);
+ }
+
+ Debug.Assert((_buckets != null) && (_count > 0), "_buckets was null but count greater than 0");
+
+ int originalCount = _count;
+ int intArrayLength = BitHelper.ToIntArrayLength(originalCount);
+
+ Span<int> span = stackalloc int[StackAllocThreshold];
+ BitHelper bitHelper = intArrayLength <= StackAllocThreshold ?
+ new BitHelper(span.Slice(0, intArrayLength), clear: true) :
+ new BitHelper(new int[intArrayLength], clear: false);
+
+ int unfoundCount = 0; // count of items in other not found in this
+ int uniqueFoundCount = 0; // count of unique items in other found in this
+
+ foreach (T item in other)
+ {
+ int index = FindItemIndex(item);
+ if (index >= 0)
+ {
+ if (!bitHelper.IsMarked(index))
+ {
+ // Item hasn't been seen yet.
+ bitHelper.MarkBit(index);
+ uniqueFoundCount++;
+ }
+ }
+ else
+ {
+ unfoundCount++;
+ if (returnIfUnfound)
+ {
+ break;
+ }
+ }
+ }
+
+ return (uniqueFoundCount, unfoundCount);
+ }
+
+ /// <summary>
+ /// Checks if equality comparers are equal. This is used for algorithms that can
+ /// speed up if it knows the other item has unique elements. I.e. if they're using
+ /// different equality comparers, then uniqueness assumption between sets break.
+ /// </summary>
+ internal static bool EqualityComparersAreEqual(HashSet<T> set1, HashSet<T> set2) => set1.Comparer.Equals(set2.Comparer);
+
+#endregion
+
+ private struct Entry
+ {
+ public int HashCode;
+ /// <summary>
+ /// 0-based index of next entry in chain: -1 means end of chain
+ /// also encodes whether this entry _itself_ is part of the free list by changing sign and subtracting 3,
+ /// so -2 means end of free list, -3 means index 0 but on free list, -4 means index 1 but on free list, etc.
+ /// </summary>
+ public int Next;
+ public T Value;
+ }
+
+ public struct Enumerator : IEnumerator<T>
+ {
+ private readonly HashSet<T> _hashSet;
+ private readonly int _version;
+ private int _index;
+ private T _current;
+
+ internal Enumerator(HashSet<T> hashSet)
+ {
+ _hashSet = hashSet;
+ _version = hashSet._version;
+ _index = 0;
+ _current = default!;
+ }
+
+ public bool MoveNext()
+ {
+ if (_version != _hashSet._version)
+ {
+ ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
+ }
+
+ // Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends.
+ // dictionary.count+1 could be negative if dictionary.count is int.MaxValue
+ while ((uint)_index < (uint)_hashSet._count)
+ {
+ ref Entry entry = ref _hashSet._entries![_index++];
+ if (entry.Next >= -1)
+ {
+ _current = entry.Value;
+ return true;
+ }
+ }
+
+ _index = _hashSet._count + 1;
+ _current = default!;
+ return false;
+ }
+
+ public T Current => _current;
+
+ public void Dispose() { }
+
+ object? IEnumerator.Current
+ {
+ get
+ {
+ if (_index == 0 || (_index == _hashSet._count + 1))
+ {
+ ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
+ }
+
+ return _current;
+ }
+ }
+
+ void IEnumerator.Reset()
+ {
+ if (_version != _hashSet._version)
+ {
+ ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
+ }
+
+ _index = 0;
+ _current = default!;
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Collections.Generic
+{
+ /// <summary>Equality comparer for hashsets of hashsets</summary>
+ internal sealed class HashSetEqualityComparer<T> : IEqualityComparer<HashSet<T>?>
+ {
+ public bool Equals(HashSet<T>? x, HashSet<T>? y)
+ {
+ // If they're the exact same instance, they're equal.
+ if (ReferenceEquals(x, y))
+ {
+ return true;
+ }
+
+ // They're not both null, so if either is null, they're not equal.
+ if (x == null || y == null)
+ {
+ return false;
+ }
+
+ EqualityComparer<T> defaultComparer = EqualityComparer<T>.Default;
+
+ // If both sets use the same comparer, they're equal if they're the same
+ // size and one is a "subset" of the other.
+ if (HashSet<T>.EqualityComparersAreEqual(x, y))
+ {
+ return x.Count == y.Count && y.IsSubsetOfHashSetWithSameComparer(x);
+ }
+
+ // Otherwise, do an O(N^2) match.
+ foreach (T yi in y)
+ {
+ bool found = false;
+ foreach (T xi in x)
+ {
+ if (defaultComparer.Equals(yi, xi))
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public int GetHashCode(HashSet<T>? obj)
+ {
+ int hashCode = 0; // default to 0 for null/empty set
+
+ if (obj != null)
+ {
+ foreach (T t in obj)
+ {
+ if (t != null)
+ {
+ hashCode ^= t.GetHashCode(); // same hashcode as as default comparer
+ }
+ }
+ }
+
+ return hashCode;
+ }
+
+ // Equals method for the comparer itself.
+ public override bool Equals(object? obj) => obj is HashSetEqualityComparer<T>;
+
+ public override int GetHashCode() => EqualityComparer<T>.Default.GetHashCode();
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Collections.Generic
+{
+ /// <summary>
+ /// Used internally to control behavior of insertion into a <see cref="Dictionary{TKey, TValue}"/> or <see cref="HashSet{T}"/>.
+ /// </summary>
+ internal enum InsertionBehavior : byte
+ {
+ /// <summary>
+ /// The default insertion behavior.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// Specifies that an existing entry with the same key should be overwritten if encountered.
+ /// </summary>
+ OverwriteExisting = 1,
+
+ /// <summary>
+ /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown.
+ /// </summary>
+ ThrowOnExisting = 2
+ }
+}
if (isJapaneseCalendar &&
!LocalAppContextSwitches.FormatJapaneseFirstYearAsANumber &&
year == 1 &&
- ((i + tokenLen < format.Length && format[i + tokenLen] == DateTimeFormatInfoScanner.CJKYearSuff[0]) ||
- (i + tokenLen < format.Length - 1 && format[i + tokenLen] == '\'' && format[i + tokenLen + 1] == DateTimeFormatInfoScanner.CJKYearSuff[0])))
+ ((i + tokenLen < format.Length && format[i + tokenLen] == DateTimeFormatInfoScanner.CJKYearSuff) ||
+ (i + tokenLen < format.Length - 1 && format[i + tokenLen] == '\'' && format[i + tokenLen + 1] == DateTimeFormatInfoScanner.CJKYearSuff)))
{
// We are formatting a Japanese date with year equals 1 and the year number is followed by the year sign \u5e74
// In Japanese dates, the first year in the era is not formatted as a number 1 instead it is formatted as \u5143 which means
internal const char IgnorableSymbolChar = '\xe001';
// Known CJK suffix
- internal const string CJKYearSuff = "\u5e74";
- internal const string CJKMonthSuff = "\u6708";
- internal const string CJKDaySuff = "\u65e5";
+ internal const char CJKYearSuff = '\u5e74';
+ internal const char CJKMonthSuff = '\u6708';
+ internal const char CJKDaySuff = '\u65e5';
- internal const string KoreanYearSuff = "\ub144";
- internal const string KoreanMonthSuff = "\uc6d4";
- internal const string KoreanDaySuff = "\uc77c";
+ internal const char KoreanYearSuff = '\ub144';
+ internal const char KoreanMonthSuff = '\uc6d4';
+ internal const char KoreanDaySuff = '\uc77c';
- internal const string KoreanHourSuff = "\uc2dc";
- internal const string KoreanMinuteSuff = "\ubd84";
- internal const string KoreanSecondSuff = "\ucd08";
+ internal const char KoreanHourSuff = '\uc2dc';
+ internal const char KoreanMinuteSuff = '\ubd84';
+ internal const char KoreanSecondSuff = '\ucd08';
- internal const string CJKHourSuff = "\u6642";
- internal const string ChineseHourSuff = "\u65f6";
+ internal const char CJKHourSuff = '\u6642';
+ internal const char ChineseHourSuff = '\u65f6';
- internal const string CJKMinuteSuff = "\u5206";
- internal const string CJKSecondSuff = "\u79d2";
+ internal const char CJKMinuteSuff = '\u5206';
+ internal const char CJKSecondSuff = '\u79d2';
- // The collection fo date words & postfix.
+ // The collection for date words & postfix.
internal List<string> m_dateWords = new List<string>();
- // Hashtable for the known words.
- private static volatile Dictionary<string, string>? s_knownWords;
-
- private static Dictionary<string, string> KnownWords =>
- s_knownWords ??=
- new Dictionary<string, string>(16)
- {
- // Add known words into the hash table.
-
- // Skip these special symbols.
- { "/", string.Empty },
- { "-", string.Empty },
- { ".", string.Empty },
-
- // Skip known CJK suffixes.
- { CJKYearSuff, string.Empty },
- { CJKMonthSuff, string.Empty },
- { CJKDaySuff, string.Empty },
- { KoreanYearSuff, string.Empty },
- { KoreanMonthSuff, string.Empty },
- { KoreanDaySuff, string.Empty },
- { KoreanHourSuff, string.Empty },
- { KoreanMinuteSuff, string.Empty },
- { KoreanSecondSuff, string.Empty },
- { CJKHourSuff, string.Empty },
- { ChineseHourSuff, string.Empty },
- { CJKMinuteSuff, string.Empty },
- { CJKSecondSuff, string.Empty }
- };
////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////
internal void AddDateWordOrPostfix(string? formatPostfix, string str)
{
- if (str.Length > 0)
+ if (str.Length == 0)
{
- // Some cultures use . like an abbreviation
- if (str.Equals("."))
+ return;
+ }
+
+ if (str.Length == 1)
+ {
+ switch (str[0])
{
- AddIgnorableSymbols(".");
- return;
+ // Some cultures use . like an abbreviation
+ case '.':
+ AddIgnorableSymbols(".");
+ return;
+
+ // Skip these special symbols.
+ case '/':
+ case '-':
+ return;
+
+ // Skip known CJK suffixes.
+ case CJKYearSuff:
+ case CJKMonthSuff:
+ case CJKDaySuff:
+ case KoreanYearSuff:
+ case KoreanMonthSuff:
+ case KoreanDaySuff:
+ case KoreanHourSuff:
+ case KoreanMinuteSuff:
+ case KoreanSecondSuff:
+ case CJKHourSuff:
+ case ChineseHourSuff:
+ case CJKMinuteSuff:
+ case CJKSecondSuff:
+ return;
}
+ }
- if (!KnownWords.TryGetValue(str, out _))
+ m_dateWords ??= new List<string>();
+
+ if (formatPostfix == "MMMM")
+ {
+ // Add the word into the ArrayList as "\xfffe" + real month postfix.
+ string temp = MonthPostfixChar + str;
+ if (!m_dateWords.Contains(temp))
+ {
+ m_dateWords.Add(temp);
+ }
+ }
+ else
+ {
+ if (!m_dateWords.Contains(str))
{
- m_dateWords ??= new List<string>();
+ m_dateWords.Add(str);
+ }
- if (formatPostfix == "MMMM")
- {
- // Add the word into the ArrayList as "\xfffe" + real month postfix.
- string temp = MonthPostfixChar + str;
- if (!m_dateWords.Contains(temp))
- {
- m_dateWords.Add(temp);
- }
- }
- else
+ if (str[^1] == '.')
+ {
+ // Old version ignore the trailing dot in the date words. Support this as well.
+ string strWithoutDot = str[0..^1];
+ if (!m_dateWords.Contains(strWithoutDot))
{
- if (!m_dateWords.Contains(str))
- {
- m_dateWords.Add(str);
- }
- if (str[^1] == '.')
- {
- // Old version ignore the trailing dot in the date words. Support this as well.
- string strWithoutDot = str[0..^1];
- if (!m_dateWords.Contains(strWithoutDot))
- {
- m_dateWords.Add(strWithoutDot);
- }
- }
+ m_dateWords.Add(strWithoutDot);
}
}
}
// we don't need the UseDigitPrefixInTokens since it is slower.
switch (array[i][index])
{
- case '\x6708': // CJKMonthSuff
- case '\xc6d4': // KoreanMonthSuff
+ case CJKMonthSuff:
+ case KoreanMonthSuff:
return false;
}
}
// Starting with Windows 8, the CJK months for some cultures looks like: "1' \x6708'"
// instead of just "1\x6708"
if (array[i][index] == '\'' && array[i][index + 1] == ' ' &&
- array[i][index + 2] == '\x6708' && array[i][index + 3] == '\'')
+ array[i][index + 2] == CJKMonthSuff && array[i][index + 3] == '\'')
{
return false;
}