// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; namespace System.Collections.Generic { [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 { // 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; #if TARGET_64BIT private ulong _fastModMultiplier; #endif private int _count; private int _freeList; private int _freeCount; private int _version; private IEqualityComparer<TKey>? _comparer; private KeyCollection? _keys; private ValueCollection? _values; private const int StartOfFreeList = -3; public Dictionary() : this(0, null) { } public Dictionary(int capacity) : this(capacity, null) { } public Dictionary(IEqualityComparer<TKey>? comparer) : this(0, comparer) { } public Dictionary(int capacity, IEqualityComparer<TKey>? comparer) { if (capacity < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); } if (capacity > 0) { Initialize(capacity); } // For reference types, we always want to store a comparer instance, either // the one provided, or if one wasn't provided, the default (accessing // EqualityComparer<TKey>.Default with shared generics on every dictionary // access can add measurable overhead). For value types, if no comparer is // provided, or if the default is provided, we'd prefer to use // EqualityComparer<TKey>.Default.Equals on every use, enabling the JIT to // devirtualize and possibly inline the operation. if (!typeof(TKey).IsValueType) { _comparer = comparer ?? EqualityComparer<TKey>.Default; // Special-case EqualityComparer<string>.Default, StringComparer.Ordinal, and StringComparer.OrdinalIgnoreCase. // We use a non-randomized comparer for improved perf, falling back to a randomized comparer if the // hash buckets become unbalanced. if (typeof(TKey) == typeof(string) && NonRandomizedStringEqualityComparer.GetStringComparer(_comparer!) is IEqualityComparer<string> stringComparer) { _comparer = (IEqualityComparer<TKey>)stringComparer; } } else if (comparer is not null && // first check for null to avoid forcing default comparer instantiation unnecessarily comparer != EqualityComparer<TKey>.Default) { _comparer = comparer; } } public Dictionary(IDictionary<TKey, TValue> dictionary) : this(dictionary, null) { } public Dictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey>? comparer) : this(dictionary?.Count ?? 0, comparer) { if (dictionary == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); } AddRange(dictionary); } public Dictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection) : this(collection, null) { } public Dictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey>? comparer) : this((collection as ICollection<KeyValuePair<TKey, TValue>>)?.Count ?? 0, comparer) { if (collection == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); } AddRange(collection); } private void AddRange(IEnumerable<KeyValuePair<TKey, TValue>> enumerable) { // It is likely that the passed-in enumerable is Dictionary<TKey,TValue>. When this is the case, // avoid the enumerator allocation and overhead by looping through the entries array directly. // We only do this when dictionary is Dictionary<TKey,TValue> and not a subclass, to maintain // back-compat with subclasses that may have overridden the enumerator behavior. if (enumerable.GetType() == typeof(Dictionary<TKey, TValue>)) { Dictionary<TKey, TValue> source = (Dictionary<TKey, TValue>)enumerable; if (source.Count == 0) { // Nothing to copy, all done return; } // This is not currently a true .AddRange as it needs to be an initialized dictionary // of the correct size, and also an empty dictionary with no current entities (and no argument checks). Debug.Assert(source._entries is not null); Debug.Assert(_entries is not null); Debug.Assert(_entries.Length >= source.Count); Debug.Assert(_count == 0); Entry[] oldEntries = source._entries; if (source._comparer == _comparer) { // If comparers are the same, we can copy _entries without rehashing. CopyEntries(oldEntries, source._count); return; } // Comparers differ need to rehash all the entries via Add int count = source._count; for (int i = 0; i < count; i++) { // Only copy if an entry if (oldEntries[i].next >= -1) { Add(oldEntries[i].key, oldEntries[i].value); } } return; } // We similarly special-case KVP<>[] and List<KVP<>>, as they're commonly used to seed dictionaries, and // we want to avoid the enumerator costs (e.g. allocation) for them as well. Extract a span if possible. ReadOnlySpan<KeyValuePair<TKey, TValue>> span; if (enumerable is KeyValuePair<TKey, TValue>[] array) { span = array; } else if (enumerable.GetType() == typeof(List<KeyValuePair<TKey, TValue>>)) { span = CollectionsMarshal.AsSpan((List<KeyValuePair<TKey, TValue>>)enumerable); } else { // Fallback path for all other enumerables foreach (KeyValuePair<TKey, TValue> pair in enumerable) { Add(pair.Key, pair.Value); } return; } // We got a span. Add the elements to the dictionary. foreach (KeyValuePair<TKey, TValue> pair in span) { Add(pair.Key, pair.Value); } } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] [EditorBrowsable(EditorBrowsableState.Never)] protected Dictionary(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 resonable 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. HashHelpers.SerializationInfoTable.Add(this, info); } public IEqualityComparer<TKey> Comparer { get { if (typeof(TKey) == typeof(string)) { Debug.Assert(_comparer is not null, "The comparer should never be null for a reference type."); return (IEqualityComparer<TKey>)IInternalStringEqualityComparer.GetUnderlyingEqualityComparer((IEqualityComparer<string?>)_comparer); } else { return _comparer ?? EqualityComparer<TKey>.Default; } } } public int Count => _count - _freeCount; /// <summary> /// Gets the total numbers of elements the internal data structure can hold without resizing. /// </summary> public int Capacity => _entries?.Length ?? 0; public KeyCollection Keys => _keys ??= new KeyCollection(this); ICollection<TKey> IDictionary<TKey, TValue>.Keys => Keys; IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => Keys; public ValueCollection Values => _values ??= new ValueCollection(this); ICollection<TValue> IDictionary<TKey, TValue>.Values => Values; IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => Values; public TValue this[TKey key] { get { ref TValue value = ref FindValue(key); if (!Unsafe.IsNullRef(ref value)) { return value; } ThrowHelper.ThrowKeyNotFoundException(key); return default; } set { bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting); Debug.Assert(modified); } } public void Add(TKey key, TValue value) { bool modified = TryInsert(key, value, InsertionBehavior.ThrowOnExisting); 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); bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> keyValuePair) { ref TValue value = ref FindValue(keyValuePair.Key); if (!Unsafe.IsNullRef(ref value) && EqualityComparer<TValue>.Default.Equals(value, keyValuePair.Value)) { return true; } return false; } bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> keyValuePair) { ref TValue value = ref FindValue(keyValuePair.Key); if (!Unsafe.IsNullRef(ref value) && EqualityComparer<TValue>.Default.Equals(value, keyValuePair.Value)) { Remove(keyValuePair.Key); return true; } return false; } 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); _count = 0; _freeList = -1; _freeCount = 0; Array.Clear(_entries, 0, count); } } public bool ContainsKey(TKey key) => !Unsafe.IsNullRef(ref FindValue(key)); public bool ContainsValue(TValue value) { Entry[]? entries = _entries; if (value == null) { for (int i = 0; i < _count; i++) { if (entries![i].next >= -1 && entries[i].value == null) { return true; } } } else if (typeof(TValue).IsValueType) { // 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)) { return true; } } } 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++) { if (entries![i].next >= -1 && defaultComparer.Equals(entries[i].value, value)) { return true; } } } return false; } private void CopyTo(KeyValuePair<TKey, TValue>[] array, int index) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if ((uint)index > (uint)array.Length) { ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); } if (array.Length - index < Count) { ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); } int count = _count; Entry[]? entries = _entries; for (int i = 0; i < count; i++) { if (entries![i].next >= -1) { array[index++] = new KeyValuePair<TKey, TValue>(entries[i].key, entries[i].value); } } } public Enumerator GetEnumerator() => new Enumerator(this, Enumerator.KeyValuePair); IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => Count == 0 ? GenericEmptyEnumerator<KeyValuePair<TKey, TValue>>.Instance : GetEnumerator(); [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] [EditorBrowsable(EditorBrowsableState.Never)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.info); } info.AddValue(VersionName, _version); info.AddValue(ComparerName, Comparer, typeof(IEqualityComparer<TKey>)); info.AddValue(HashSizeName, _buckets == null ? 0 : _buckets.Length); // This is the length of the bucket array if (_buckets != null) { var array = new KeyValuePair<TKey, TValue>[Count]; CopyTo(array, 0); info.AddValue(KeyValuePairsName, array, typeof(KeyValuePair<TKey, TValue>[])); } } internal ref TValue FindValue(TKey key) { if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } ref Entry entry = ref Unsafe.NullRef<Entry>(); if (_buckets != null) { Debug.Assert(_entries != null, "expected entries to be != null"); IEqualityComparer<TKey>? comparer = _comparer; if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types comparer == null) { // TODO: Replace with just key.GetHashCode once https://github.com/dotnet/runtime/issues/117521 is resolved. uint hashCode = (uint)EqualityComparer<TKey>.Default.GetHashCode(key); int i = GetBucket(hashCode); Entry[]? entries = _entries; uint collisionCount = 0; // ValueType: Devirtualize with EqualityComparer<TKey>.Default intrinsic i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. do { // Test in if to drop range check for following array access if ((uint)i >= (uint)entries.Length) { goto ReturnNotFound; } entry = ref entries[i]; if (entry.hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entry.key, key)) { goto ReturnFound; } i = entry.next; 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; } else { Debug.Assert(comparer is not null); uint hashCode = (uint)comparer.GetHashCode(key); int i = GetBucket(hashCode); Entry[]? entries = _entries; uint collisionCount = 0; i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. do { // Test in if to drop range check for following array access if ((uint)i >= (uint)entries.Length) { goto ReturnNotFound; } entry = ref entries[i]; if (entry.hashCode == hashCode && comparer.Equals(entry.key, key)) { goto ReturnFound; } i = entry.next; 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; } } goto ReturnNotFound; ConcurrentOperation: ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); ReturnFound: ref TValue value = ref entry.value; Return: return ref value; ReturnNotFound: value = ref Unsafe.NullRef<TValue>(); goto Return; } private int Initialize(int capacity) { int size = HashHelpers.GetPrime(capacity); int[] buckets = new int[size]; Entry[] entries = new Entry[size]; // Assign member variables after both arrays allocated to guard against corruption from OOM if second fails _freeList = -1; #if TARGET_64BIT _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)size); #endif _buckets = buckets; _entries = entries; return size; } private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) { // NOTE: this method is mirrored in CollectionsMarshal.GetValueRefOrAddDefault below. // If you make any changes here, make sure to keep that version in sync as well. if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } if (_buckets == null) { Initialize(0); } Debug.Assert(_buckets != null); Entry[]? entries = _entries; Debug.Assert(entries != null, "expected entries to be non-null"); IEqualityComparer<TKey>? comparer = _comparer; Debug.Assert(comparer is not null || typeof(TKey).IsValueType); uint hashCode = (uint)((typeof(TKey).IsValueType && comparer == null) ? key.GetHashCode() : comparer!.GetHashCode(key)); uint collisionCount = 0; ref int bucket = ref GetBucket(hashCode); int i = bucket - 1; // Value in _buckets is 1-based if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types comparer == null) { // ValueType: Devirtualize with EqualityComparer<TKey>.Default intrinsic while ((uint)i < (uint)entries.Length) { if (entries[i].hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entries[i].key, key)) { if (behavior == InsertionBehavior.OverwriteExisting) { entries[i].value = value; return true; } if (behavior == InsertionBehavior.ThrowOnExisting) { ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key); } return false; } i = entries[i].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(); } } } else { Debug.Assert(comparer is not null); while ((uint)i < (uint)entries.Length) { if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { if (behavior == InsertionBehavior.OverwriteExisting) { entries[i].value = value; return true; } if (behavior == InsertionBehavior.ThrowOnExisting) { ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key); } return false; } i = entries[i].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(); } } } int index; if (_freeCount > 0) { index = _freeList; Debug.Assert((StartOfFreeList - entries[_freeList].next) >= -1, "shouldn't overflow because `next` cannot underflow"); _freeList = StartOfFreeList - entries[_freeList].next; _freeCount--; } else { int count = _count; if (count == entries.Length) { Resize(); bucket = ref GetBucket(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.key = key; entry.value = value; bucket = index + 1; // Value in _buckets is 1-based _version++; // Value types never rehash if (!typeof(TKey).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. Resize(entries.Length, true); } return true; } /// <summary> /// Gets an instance of a type that may be used to perform operations on the current <see cref="Dictionary{TKey, TValue}"/> /// using a <typeparamref name="TAlternateKey"/> as a key instead of a <typeparamref name="TKey"/>. /// </summary> /// <typeparam name="TAlternateKey">The alternate type of a key for performing lookups.</typeparam> /// <returns>The created lookup instance.</returns> /// <exception cref="InvalidOperationException">The dictionary's comparer is not compatible with <typeparamref name="TAlternateKey"/>.</exception> /// <remarks> /// The dictionary must be using a comparer that implements <see cref="IAlternateEqualityComparer{TAlternateKey, TKey}"/> with /// <typeparamref name="TAlternateKey"/> and <typeparamref name="TKey"/>. If it doesn't, an exception will be thrown. /// </remarks> public AlternateLookup<TAlternateKey> GetAlternateLookup<TAlternateKey>() where TAlternateKey : notnull, allows ref struct { if (!AlternateLookup<TAlternateKey>.IsCompatibleKey(this)) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_IncompatibleComparer); } return new AlternateLookup<TAlternateKey>(this); } /// <summary> /// Gets an instance of a type that may be used to perform operations on the current <see cref="Dictionary{TKey, TValue}"/> /// using a <typeparamref name="TAlternateKey"/> as a key instead of a <typeparamref name="TKey"/>. /// </summary> /// <typeparam name="TAlternateKey">The alternate type of a key for performing lookups.</typeparam> /// <param name="lookup">The created lookup instance when the method returns true, or a default instance that should not be used if the method returns false.</param> /// <returns>true if a lookup could be created; otherwise, false.</returns> /// <remarks> /// The dictionary must be using a comparer that implements <see cref="IAlternateEqualityComparer{TAlternateKey, TKey}"/> with /// <typeparamref name="TAlternateKey"/> and <typeparamref name="TKey"/>. If it doesn't, the method will return false. /// </remarks> public bool TryGetAlternateLookup<TAlternateKey>( out AlternateLookup<TAlternateKey> lookup) where TAlternateKey : notnull, allows ref struct { if (AlternateLookup<TAlternateKey>.IsCompatibleKey(this)) { lookup = new AlternateLookup<TAlternateKey>(this); return true; } lookup = default; return false; } /// <summary> /// Provides a type that may be used to perform operations on a <see cref="Dictionary{TKey, TValue}"/> /// using a <typeparamref name="TAlternateKey"/> as a key instead of a <typeparamref name="TKey"/>. /// </summary> /// <typeparam name="TAlternateKey">The alternate type of a key for performing lookups.</typeparam> public readonly struct AlternateLookup<TAlternateKey> where TAlternateKey : notnull, allows ref struct { /// <summary>Initialize the instance. The dictionary must have already been verified to have a compatible comparer.</summary> internal AlternateLookup(Dictionary<TKey, TValue> dictionary) { Debug.Assert(dictionary is not null); Debug.Assert(IsCompatibleKey(dictionary)); Dictionary = dictionary; } /// <summary>Gets the <see cref="Dictionary{TKey, TValue}"/> against which this instance performs operations.</summary> public Dictionary<TKey, TValue> Dictionary { get; } /// <summary>Gets or sets the value associated with the specified alternate key.</summary> /// <param name="key">The alternate key of the value to get or set.</param> /// <value> /// The value associated with the specified alternate key. If the specified alternate key is not found, a get operation throws /// a <see cref="KeyNotFoundException"/>, and a set operation creates a new element with the specified key. /// </value> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> /// <exception cref="KeyNotFoundException">The property is retrieved and alternate key does not exist in the collection.</exception> public TValue this[TAlternateKey key] { get { ref TValue value = ref FindValue(key, out _); if (Unsafe.IsNullRef(ref value)) { ThrowHelper.ThrowKeyNotFoundException(GetAlternateComparer(Dictionary).Create(key)); } return value; } set => GetValueRefOrAddDefault(key, out _) = value; } /// <summary>Checks whether the dictionary has a comparer compatible with <typeparamref name="TAlternateKey"/>.</summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsCompatibleKey(Dictionary<TKey, TValue> dictionary) { Debug.Assert(dictionary is not null); return dictionary._comparer is IAlternateEqualityComparer<TAlternateKey, TKey>; } /// <summary>Gets the dictionary's alternate comparer. The dictionary must have already been verified as compatible.</summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static IAlternateEqualityComparer<TAlternateKey, TKey> GetAlternateComparer(Dictionary<TKey, TValue> dictionary) { Debug.Assert(IsCompatibleKey(dictionary)); return Unsafe.As<IAlternateEqualityComparer<TAlternateKey, TKey>>(dictionary._comparer)!; } /// <summary>Gets the value associated with the specified alternate key.</summary> /// <param name="key">The alternate key of the value to get.</param> /// <param name="value"> /// When this method returns, contains the value associated with the specified key, if the key is found; /// otherwise, the default value for the type of the value parameter. /// </param> /// <returns><see langword="true"/> if an entry was found; otherwise, <see langword="false"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) { ref TValue valueRef = ref FindValue(key, out _); if (!Unsafe.IsNullRef(ref valueRef)) { value = valueRef; return true; } value = default; return false; } /// <summary>Gets the value associated with the specified alternate key.</summary> /// <param name="key">The alternate key of the value to get.</param> /// <param name="actualKey"> /// When this method returns, contains the actual key associated with the alternate key, if the key is found; /// otherwise, the default value for the type of the key parameter. /// </param> /// <param name="value"> /// When this method returns, contains the value associated with the specified key, if the key is found; /// otherwise, the default value for the type of the value parameter. /// </param> /// <returns><see langword="true"/> if an entry was found; otherwise, <see langword="false"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TKey actualKey, [MaybeNullWhen(false)] out TValue value) { ref TValue valueRef = ref FindValue(key, out actualKey); if (!Unsafe.IsNullRef(ref valueRef)) { value = valueRef; Debug.Assert(actualKey is not null); return true; } value = default; return false; } /// <summary>Determines whether the <see cref="Dictionary{TKey, TValue}"/> contains the specified alternate key.</summary> /// <param name="key">The alternate key to check.</param> /// <returns><see langword="true"/> if the key is in the dictionary; otherwise, <see langword="false"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> public bool ContainsKey(TAlternateKey key) => !Unsafe.IsNullRef(ref FindValue(key, out _)); /// <summary>Finds the entry associated with the specified alternate key.</summary> /// <param name="key">The alternate key.</param> /// <param name="actualKey">The actual key, if found.</param> /// <returns>A reference to the value associated with the key, if found; otherwise, a null reference.</returns> internal ref TValue FindValue(TAlternateKey key, [MaybeNullWhen(false)] out TKey actualKey) { Dictionary<TKey, TValue> dictionary = Dictionary; IAlternateEqualityComparer<TAlternateKey, TKey> comparer = GetAlternateComparer(dictionary); ref Entry entry = ref Unsafe.NullRef<Entry>(); if (dictionary._buckets != null) { Debug.Assert(dictionary._entries != null, "expected entries to be != null"); uint hashCode = (uint)comparer.GetHashCode(key); int i = dictionary.GetBucket(hashCode); Entry[]? entries = dictionary._entries; uint collisionCount = 0; 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 // Test in if to drop range check for following array access if ((uint)i >= (uint)entries.Length) { goto ReturnNotFound; } entry = ref entries[i]; if (entry.hashCode == hashCode && comparer.Equals(key, entry.key)) { goto ReturnFound; } i = entry.next; 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; } goto ReturnNotFound; ConcurrentOperation: ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); ReturnFound: ref TValue value = ref entry.value; actualKey = entry.key; Return: return ref value; ReturnNotFound: value = ref Unsafe.NullRef<TValue>(); actualKey = default!; goto Return; } /// <summary>Removes the value with the specified alternate key from the <see cref="Dictionary{TKey, TValue}"/>.</summary> /// <param name="key">The alternate key of the element to remove.</param> /// <returns>true if the element is successfully found and removed; otherwise, false.</returns> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> public bool Remove(TAlternateKey key) => Remove(key, out _, out _); /// <summary> /// Removes the value with the specified alternate key from the <see cref="Dictionary{TKey, TValue}"/>, /// and copies the element to the value parameter. /// </summary> /// <param name="key">The alternate key of the element to remove.</param> /// <param name="actualKey">The removed key.</param> /// <param name="value">The removed element.</param> /// <returns>true if the element is successfully found and removed; otherwise, false.</returns> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> public bool Remove(TAlternateKey key, [MaybeNullWhen(false)] out TKey actualKey, [MaybeNullWhen(false)] out TValue value) { Dictionary<TKey, TValue> dictionary = Dictionary; IAlternateEqualityComparer<TAlternateKey, TKey> comparer = GetAlternateComparer(dictionary); if (dictionary._buckets != null) { Debug.Assert(dictionary._entries != null, "entries should be non-null"); uint collisionCount = 0; uint hashCode = (uint)comparer.GetHashCode(key); ref int bucket = ref dictionary.GetBucket(hashCode); Entry[]? entries = dictionary._entries; int last = -1; int i = bucket - 1; // Value in buckets is 1-based while ((uint)i < (uint)entries.Length) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && comparer.Equals(key, entry.key)) { if (last < 0) { bucket = entry.next + 1; // Value in buckets is 1-based } else { entries[last].next = entry.next; } actualKey = entry.key; value = entry.value; Debug.Assert((StartOfFreeList - dictionary._freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); entry.next = StartOfFreeList - dictionary._freeList; if (RuntimeHelpers.IsReferenceOrContainsReferences<TKey>()) { entry.key = default!; } if (RuntimeHelpers.IsReferenceOrContainsReferences<TValue>()) { entry.value = default!; } dictionary._freeList = i; dictionary._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(); } } } actualKey = default; value = default; return false; } /// <summary>Attempts to add the specified key and value to the dictionary.</summary> /// <param name="key">The alternate key of the element to add.</param> /// <param name="value">The value of the element to add.</param> /// <returns>true if the key/value pair was added to the dictionary successfully; otherwise, false.</returns> /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception> public bool TryAdd(TAlternateKey key, TValue value) { ref TValue? slot = ref GetValueRefOrAddDefault(key, out bool exists); if (!exists) { slot = value; return true; } return false; } /// <inheritdoc cref="CollectionsMarshal.GetValueRefOrAddDefault{TKey, TValue}(Dictionary{TKey, TValue}, TKey, out bool)"/> internal ref TValue? GetValueRefOrAddDefault(TAlternateKey key, out bool exists) { // NOTE: this method is a mirror of GetValueRefOrAddDefault above. Keep it in sync. Dictionary<TKey, TValue> dictionary = Dictionary; IAlternateEqualityComparer<TAlternateKey, TKey> comparer = GetAlternateComparer(dictionary); if (dictionary._buckets == null) { dictionary.Initialize(0); } Debug.Assert(dictionary._buckets != null); Entry[]? entries = dictionary._entries; Debug.Assert(entries != null, "expected entries to be non-null"); uint hashCode = (uint)comparer.GetHashCode(key); uint collisionCount = 0; ref int bucket = ref dictionary.GetBucket(hashCode); int i = bucket - 1; // Value in _buckets is 1-based Debug.Assert(comparer is not null); while ((uint)i < (uint)entries.Length) { if (entries[i].hashCode == hashCode && comparer.Equals(key, entries[i].key)) { exists = true; return ref entries[i].value!; } i = entries[i].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(); } } TKey actualKey = comparer.Create(key); if (actualKey is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } int index; if (dictionary._freeCount > 0) { index = dictionary._freeList; Debug.Assert((StartOfFreeList - entries[dictionary._freeList].next) >= -1, "shouldn't overflow because `next` cannot underflow"); dictionary._freeList = StartOfFreeList - entries[dictionary._freeList].next; dictionary._freeCount--; } else { int count = dictionary._count; if (count == entries.Length) { dictionary.Resize(); bucket = ref dictionary.GetBucket(hashCode); } index = count; dictionary._count = count + 1; entries = dictionary._entries; } ref Entry entry = ref entries![index]; entry.hashCode = hashCode; entry.next = bucket - 1; // Value in _buckets is 1-based entry.key = actualKey; entry.value = default!; bucket = index + 1; // Value in _buckets is 1-based dictionary._version++; // Value types never rehash if (!typeof(TKey).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. dictionary.Resize(entries.Length, true); exists = false; // At this point the entries array has been resized, so the current reference we have is no longer valid. // We're forced to do a new lookup and return an updated reference to the new entry instance. This new // lookup is guaranteed to always find a value though and it will never return a null reference here. ref TValue? value = ref dictionary.FindValue(actualKey)!; Debug.Assert(!Unsafe.IsNullRef(ref value), "the lookup result cannot be a null ref here"); return ref value; } exists = false; return ref entry.value!; } } /// <summary> /// A helper class containing APIs exposed through <see cref="CollectionsMarshal"/> or <see cref="CollectionExtensions"/>. /// These methods are relatively niche and only used in specific scenarios, so adding them in a separate type avoids /// the additional overhead on each <see cref="Dictionary{TKey, TValue}"/> instantiation, especially in AOT scenarios. /// </summary> internal static class CollectionsMarshalHelper { /// <inheritdoc cref="CollectionsMarshal.GetValueRefOrAddDefault{TKey, TValue}(Dictionary{TKey, TValue}, TKey, out bool)"/> public static ref TValue? GetValueRefOrAddDefault(Dictionary<TKey, TValue> dictionary, TKey key, out bool exists) { // NOTE: this method is mirrored by Dictionary<TKey, TValue>.TryInsert above. // If you make any changes here, make sure to keep that version in sync as well. if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } if (dictionary._buckets == null) { dictionary.Initialize(0); } Debug.Assert(dictionary._buckets != null); Entry[]? entries = dictionary._entries; Debug.Assert(entries != null, "expected entries to be non-null"); IEqualityComparer<TKey>? comparer = dictionary._comparer; Debug.Assert(comparer is not null || typeof(TKey).IsValueType); uint hashCode = (uint)((typeof(TKey).IsValueType && comparer == null) ? key.GetHashCode() : comparer!.GetHashCode(key)); uint collisionCount = 0; ref int bucket = ref dictionary.GetBucket(hashCode); int i = bucket - 1; // Value in _buckets is 1-based if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types comparer == null) { // ValueType: Devirtualize with EqualityComparer<TKey>.Default intrinsic while ((uint)i < (uint)entries.Length) { if (entries[i].hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entries[i].key, key)) { exists = true; return ref entries[i].value!; } i = entries[i].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(); } } } else { Debug.Assert(comparer is not null); while ((uint)i < (uint)entries.Length) { if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) { exists = true; return ref entries[i].value!; } i = entries[i].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(); } } } int index; if (dictionary._freeCount > 0) { index = dictionary._freeList; Debug.Assert((StartOfFreeList - entries[dictionary._freeList].next) >= -1, "shouldn't overflow because `next` cannot underflow"); dictionary._freeList = StartOfFreeList - entries[dictionary._freeList].next; dictionary._freeCount--; } else { int count = dictionary._count; if (count == entries.Length) { dictionary.Resize(); bucket = ref dictionary.GetBucket(hashCode); } index = count; dictionary._count = count + 1; entries = dictionary._entries; } ref Entry entry = ref entries![index]; entry.hashCode = hashCode; entry.next = bucket - 1; // Value in _buckets is 1-based entry.key = key; entry.value = default!; bucket = index + 1; // Value in _buckets is 1-based dictionary._version++; // Value types never rehash if (!typeof(TKey).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. dictionary.Resize(entries.Length, true); exists = false; // At this point the entries array has been resized, so the current reference we have is no longer valid. // We're forced to do a new lookup and return an updated reference to the new entry instance. This new // lookup is guaranteed to always find a value though and it will never return a null reference here. ref TValue? value = ref dictionary.FindValue(key)!; Debug.Assert(!Unsafe.IsNullRef(ref value), "the lookup result cannot be a null ref here"); return ref value; } exists = false; return ref entry.value!; } } public virtual void OnDeserialization(object? sender) { HashHelpers.SerializationInfoTable.TryGetValue(this, out SerializationInfo? siInfo); if (siInfo == null) { // We can return immediately if this function is called twice. // Note we remove the serialization info from the table at the end of this method. return; } int realVersion = siInfo.GetInt32(VersionName); int hashsize = siInfo.GetInt32(HashSizeName); _comparer = (IEqualityComparer<TKey>)siInfo.GetValue(ComparerName, typeof(IEqualityComparer<TKey>))!; // When serialized if comparer is null, we use the default. if (hashsize != 0) { Initialize(hashsize); KeyValuePair<TKey, TValue>[]? array = (KeyValuePair<TKey, TValue>[]?) siInfo.GetValue(KeyValuePairsName, typeof(KeyValuePair<TKey, TValue>[])); if (array == null) { ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_MissingKeys); } for (int i = 0; i < array.Length; i++) { if (array[i].Key == null) { ThrowHelper.ThrowSerializationException(ExceptionResource.Serialization_NullKey); } Add(array[i].Key, array[i].Value); } } else { _buckets = null; } _version = realVersion; HashHelpers.SerializationInfoTable.Remove(this); } private void Resize() => Resize(HashHelpers.ExpandPrime(_count), false); private void Resize(int newSize, bool forceNewHashCodes) { // Value types never rehash Debug.Assert(!forceNewHashCodes || !typeof(TKey).IsValueType); Debug.Assert(_entries != null, "_entries should be non-null"); Debug.Assert(newSize >= _entries.Length); Entry[] entries = new Entry[newSize]; int count = _count; Array.Copy(_entries, entries, count); if (!typeof(TKey).IsValueType && forceNewHashCodes) { Debug.Assert(_comparer is NonRandomizedStringEqualityComparer); IEqualityComparer<TKey> comparer = _comparer = (IEqualityComparer<TKey>)((NonRandomizedStringEqualityComparer)_comparer).GetRandomizedEqualityComparer(); for (int i = 0; i < count; i++) { if (entries[i].next >= -1) { entries[i].hashCode = (uint)comparer.GetHashCode(entries[i].key); } } } // 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++) { if (entries[i].next >= -1) { ref int bucket = ref GetBucket(entries[i].hashCode); entries[i].next = bucket - 1; // Value in _buckets is 1-based bucket = i + 1; } } _entries = entries; } 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 you make any changes here, make sure to keep that version in sync as well. if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } if (_buckets != null) { Debug.Assert(_entries != null, "entries should be non-null"); uint collisionCount = 0; IEqualityComparer<TKey>? comparer = _comparer; Debug.Assert(typeof(TKey).IsValueType || comparer is not null); uint hashCode = (uint)(typeof(TKey).IsValueType && comparer == null ? key.GetHashCode() : comparer!.GetHashCode(key)); ref int bucket = ref GetBucket(hashCode); Entry[]? entries = _entries; int last = -1; int i = bucket - 1; // Value in buckets is 1-based if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types comparer == null) { while ((uint)i < (uint)entries.Length) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entry.key, key)) { 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<TKey>()) { entry.key = default!; } if (RuntimeHelpers.IsReferenceOrContainsReferences<TValue>()) { 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(); } } } else { Debug.Assert(comparer is not null); while ((uint)i < (uint)entries.Length) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && comparer.Equals(entry.key, key)) { 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<TKey>()) { entry.key = default!; } if (RuntimeHelpers.IsReferenceOrContainsReferences<TValue>()) { 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; } 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 you make any changes here, make sure to keep the other overload in sync as well. if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } if (_buckets != null) { Debug.Assert(_entries != null, "entries should be non-null"); uint collisionCount = 0; IEqualityComparer<TKey>? comparer = _comparer; Debug.Assert(typeof(TKey).IsValueType || comparer is not null); uint hashCode = (uint)(typeof(TKey).IsValueType && comparer == null ? key.GetHashCode() : comparer!.GetHashCode(key)); ref int bucket = ref GetBucket(hashCode); Entry[]? entries = _entries; int last = -1; int i = bucket - 1; // Value in buckets is 1-based if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types comparer == null) { while ((uint)i < (uint)entries.Length) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entry.key, key)) { if (last < 0) { bucket = entry.next + 1; // Value in buckets is 1-based } else { entries[last].next = entry.next; } 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; } 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(); } } } else { Debug.Assert(comparer is not null); while ((uint)i < (uint)entries.Length) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && comparer.Equals(entry.key, key)) { if (last < 0) { bucket = entry.next + 1; // Value in buckets is 1-based } else { entries[last].next = entry.next; } 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; } 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(); } } } } value = default; return false; } public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) { ref TValue valRef = ref FindValue(key); if (!Unsafe.IsNullRef(ref valRef)) { value = valRef; return true; } value = default; return false; } 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.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) { CopyTo(pairs, index); } else if (array is DictionaryEntry[] dictEntryArray) { Entry[]? entries = _entries; for (int i = 0; i < _count; i++) { if (entries![i].next >= -1) { dictEntryArray[index++] = new DictionaryEntry(entries[i].key, entries[i].value); } } } else { object[]? objects = array as object[]; if (objects == null) { ThrowHelper.ThrowArgumentException_Argument_IncompatibleArrayType(); } try { int count = _count; Entry[]? entries = _entries; for (int i = 0; i < count; i++) { if (entries![i].next >= -1) { objects[index++] = new KeyValuePair<TKey, TValue>(entries[i].key, entries[i].value); } } } catch (ArrayTypeMismatchException) { ThrowHelper.ThrowArgumentException_Argument_IncompatibleArrayType(); } } } IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<KeyValuePair<TKey, TValue>>)this).GetEnumerator(); /// <summary> /// Ensures that the dictionary can hold up to 'capacity' entries without any further expansion of its backing storage /// </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; } _version++; if (_buckets == null) { return Initialize(capacity); } int newSize = HashHelpers.GetPrime(capacity); Resize(newSize, forceNewHashCodes: false); return newSize; } /// <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. /// /// To allocate minimum size storage array, execute the following statements: /// /// dictionary.Clear(); /// dictionary.TrimExcess(); /// </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. /// </remarks> /// <exception cref="ArgumentOutOfRangeException">Passed capacity is lower than entries count.</exception> 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++; Initialize(newSize); Debug.Assert(oldEntries is not null); CopyEntries(oldEntries, oldCount); } private void CopyEntries(Entry[] entries, int count) { Debug.Assert(_entries is not null); Entry[] newEntries = _entries; int newCount = 0; for (int i = 0; i < count; i++) { uint hashCode = entries[i].hashCode; if (entries[i].next >= -1) { ref Entry entry = ref newEntries[newCount]; entry = entries[i]; ref int bucket = ref GetBucket(hashCode); entry.next = bucket - 1; // Value in _buckets is 1-based bucket = newCount + 1; newCount++; } } _count = newCount; _freeCount = 0; } bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; bool IDictionary.IsFixedSize => false; bool IDictionary.IsReadOnly => false; ICollection IDictionary.Keys => Keys; ICollection IDictionary.Values => Values; object? IDictionary.this[object key] { get { if (IsCompatibleKey(key)) { ref TValue value = ref FindValue((TKey)key); if (!Unsafe.IsNullRef(ref value)) { return value; } } return null; } set { if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } ThrowHelper.IfNullAndNullsAreIllegalThenThrow<TValue>(value, ExceptionArgument.value); try { TKey tempKey = (TKey)key; try { this[tempKey] = (TValue)value!; } catch (InvalidCastException) { ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(TValue)); } } catch (InvalidCastException) { ThrowHelper.ThrowWrongKeyTypeArgumentException(key, typeof(TKey)); } } } private static bool IsCompatibleKey(object key) { if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } return key is TKey; } void IDictionary.Add(object key, object? value) { if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } ThrowHelper.IfNullAndNullsAreIllegalThenThrow<TValue>(value, ExceptionArgument.value); try { TKey tempKey = (TKey)key; try { Add(tempKey, (TValue)value!); } catch (InvalidCastException) { ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(TValue)); } } catch (InvalidCastException) { ThrowHelper.ThrowWrongKeyTypeArgumentException(key, typeof(TKey)); } } bool IDictionary.Contains(object key) { if (IsCompatibleKey(key)) { return ContainsKey((TKey)key); } return false; } IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this, Enumerator.DictEntry); void IDictionary.Remove(object key) { if (IsCompatibleKey(key)) { Remove((TKey)key); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref int GetBucket(uint hashCode) { int[] buckets = _buckets!; #if TARGET_64BIT return ref buckets[HashHelpers.FastMod(hashCode, (uint)buckets.Length, _fastModMultiplier)]; #else return ref buckets[(uint)hashCode % buckets.Length]; #endif } 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; private int _index; private KeyValuePair<TKey, TValue> _current; private readonly int _getEnumeratorRetType; // What should Enumerator.Current return? internal const int DictEntry = 1; internal const int KeyValuePair = 2; internal Enumerator(Dictionary<TKey, TValue> dictionary, int getEnumeratorRetType) { _dictionary = dictionary; _version = dictionary._version; _index = 0; _getEnumeratorRetType = getEnumeratorRetType; _current = default; } public bool MoveNext() { if (_version != _dictionary._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)_dictionary._count) { ref Entry entry = ref _dictionary._entries![_index++]; if (entry.next >= -1) { _current = new KeyValuePair<TKey, TValue>(entry.key, entry.value); return true; } } _index = _dictionary._count + 1; _current = default; return false; } public KeyValuePair<TKey, TValue> Current => _current; public void Dispose() { } object? IEnumerator.Current { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); } if (_getEnumeratorRetType == DictEntry) { return new DictionaryEntry(_current.Key, _current.Value); } return new KeyValuePair<TKey, TValue>(_current.Key, _current.Value); } } void IEnumerator.Reset() { if (_version != _dictionary._version) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); } _index = 0; _current = default; } DictionaryEntry IDictionaryEnumerator.Entry { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); } return new DictionaryEntry(_current.Key, _current.Value); } } object IDictionaryEnumerator.Key { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); } return _current.Key; } } object? IDictionaryEnumerator.Value { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); } return _current.Value; } } } [DebuggerTypeProxy(typeof(DictionaryKeyCollectionDebugView<,>))] [DebuggerDisplay("Count = {Count}")] public sealed class KeyCollection : ICollection<TKey>, ICollection, IReadOnlyCollection<TKey> { private readonly Dictionary<TKey, TValue> _dictionary; public KeyCollection(Dictionary<TKey, TValue> dictionary) { if (dictionary == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); } _dictionary = dictionary; } public Enumerator GetEnumerator() => new Enumerator(_dictionary); public void CopyTo(TKey[] array, int index) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if (index < 0 || index > array.Length) { ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); } if (array.Length - index < _dictionary.Count) { ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); } int count = _dictionary._count; Entry[]? entries = _dictionary._entries; for (int i = 0; i < count; i++) { if (entries![i].next >= -1) array[index++] = entries[i].key; } } public int Count => _dictionary.Count; bool ICollection<TKey>.IsReadOnly => true; void ICollection<TKey>.Add(TKey item) => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet); void ICollection<TKey>.Clear() => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet); public bool Contains(TKey item) => _dictionary.ContainsKey(item); bool ICollection<TKey>.Remove(TKey item) { ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_KeyCollectionSet); return false; } IEnumerator<TKey> IEnumerable<TKey>.GetEnumerator() => Count == 0 ? SZGenericArrayEnumerator<TKey>.Empty : GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<TKey>)this).GetEnumerator(); 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) { CopyTo(keys, index); } else { object[]? objects = array as object[]; if (objects == null) { ThrowHelper.ThrowArgumentException_Argument_IncompatibleArrayType(); } int count = _dictionary._count; Entry[]? entries = _dictionary._entries; try { for (int i = 0; i < count; i++) { if (entries![i].next >= -1) objects[index++] = entries[i].key; } } catch (ArrayTypeMismatchException) { ThrowHelper.ThrowArgumentException_Argument_IncompatibleArrayType(); } } } bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; public struct Enumerator : IEnumerator<TKey>, IEnumerator { private readonly Dictionary<TKey, TValue> _dictionary; private int _index; private readonly int _version; private TKey? _currentKey; internal Enumerator(Dictionary<TKey, TValue> dictionary) { _dictionary = dictionary; _version = dictionary._version; _index = 0; _currentKey = default; } public void Dispose() { } public bool MoveNext() { if (_version != _dictionary._version) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); } while ((uint)_index < (uint)_dictionary._count) { ref Entry entry = ref _dictionary._entries![_index++]; if (entry.next >= -1) { _currentKey = entry.key; return true; } } _index = _dictionary._count + 1; _currentKey = default; return false; } public TKey Current => _currentKey!; object? IEnumerator.Current { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); } return _currentKey; } } void IEnumerator.Reset() { if (_version != _dictionary._version) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); } _index = 0; _currentKey = default; } } } [DebuggerTypeProxy(typeof(DictionaryValueCollectionDebugView<,>))] [DebuggerDisplay("Count = {Count}")] public sealed class ValueCollection : ICollection<TValue>, ICollection, IReadOnlyCollection<TValue> { private readonly Dictionary<TKey, TValue> _dictionary; public ValueCollection(Dictionary<TKey, TValue> dictionary) { if (dictionary == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary); } _dictionary = dictionary; } public Enumerator GetEnumerator() => new Enumerator(_dictionary); public void CopyTo(TValue[] array, int index) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if ((uint)index > array.Length) { ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); } if (array.Length - index < _dictionary.Count) { ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); } int count = _dictionary._count; Entry[]? entries = _dictionary._entries; for (int i = 0; i < count; i++) { if (entries![i].next >= -1) array[index++] = entries[i].value; } } public int Count => _dictionary.Count; bool ICollection<TValue>.IsReadOnly => true; void ICollection<TValue>.Add(TValue item) => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet); bool ICollection<TValue>.Remove(TValue item) { ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet); return false; } void ICollection<TValue>.Clear() => ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ValueCollectionSet); bool ICollection<TValue>.Contains(TValue item) => _dictionary.ContainsValue(item); IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => Count == 0 ? SZGenericArrayEnumerator<TValue>.Empty : GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<TValue>)this).GetEnumerator(); 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) { CopyTo(values, index); } else { object[]? objects = array as object[]; if (objects == null) { ThrowHelper.ThrowArgumentException_Argument_IncompatibleArrayType(); } int count = _dictionary._count; Entry[]? entries = _dictionary._entries; try { for (int i = 0; i < count; i++) { if (entries![i].next >= -1) objects[index++] = entries[i].value!; } } catch (ArrayTypeMismatchException) { ThrowHelper.ThrowArgumentException_Argument_IncompatibleArrayType(); } } } bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => ((ICollection)_dictionary).SyncRoot; public struct Enumerator : IEnumerator<TValue>, IEnumerator { private readonly Dictionary<TKey, TValue> _dictionary; private int _index; private readonly int _version; private TValue? _currentValue; internal Enumerator(Dictionary<TKey, TValue> dictionary) { _dictionary = dictionary; _version = dictionary._version; _index = 0; _currentValue = default; } public void Dispose() { } public bool MoveNext() { if (_version != _dictionary._version) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); } while ((uint)_index < (uint)_dictionary._count) { ref Entry entry = ref _dictionary._entries![_index++]; if (entry.next >= -1) { _currentValue = entry.value; return true; } } _index = _dictionary._count + 1; _currentValue = default; return false; } public TValue Current => _currentValue!; object? IEnumerator.Current { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); } return _currentValue; } } void IEnumerator.Reset() { if (_version != _dictionary._version) { ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); } _index = 0; _currentValue = default; } } } } }