// 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.
/*============================================================
**
**
**
**
** Purpose: Generic hash table implementation
**
** #DictionaryVersusHashtableThreadSafety
** Hashtable has multiple reader/single writer (MR/SW) thread safety built into
** certain methods and properties, whereas Dictionary doesn't. If you're
** converting framework code that formerly used Hashtable to Dictionary, it's
** important to consider whether callers may have taken a dependence on MR/SW
** thread safety. If a reader writer lock is available, then that may be used
** with a Dictionary to get the same thread safety guarantee.
**
** Reader writer locks don't exist in silverlight, so we do the following as a
** result of removing non-generic collections from silverlight:
** 1. If the Hashtable was fully synchronized, then we replace it with a
** Dictionary with full locks around reads/writes (same thread safety
** guarantee).
** 2. Otherwise, the Hashtable has the default MR/SW thread safety behavior,
** so we do one of the following on a case-by-case basis:
** a. If the race condition can be addressed by rearranging the code and using a temp
** variable (for example, it's only populated immediately after created)
** then we address the race condition this way and use Dictionary.
** b. If there's concern about degrading performance with the increased
** locking, we ifdef with FEATURE_NONGENERIC_COLLECTIONS so we can at
** least use Hashtable in the desktop build, but Dictionary with full
** locks in silverlight builds. Note that this is heavier locking than
** MR/SW, but this is the only option without rewriting (or adding back)
** the reader writer lock.
** c. If there's no performance concern (e.g. debug-only code) we
** consistently replace Hashtable with Dictionary plus full locks to
** reduce complexity.
** d. Most of serialization is dead code in silverlight. Instead of updating
** those Hashtable occurences in serialization, we carved out references
** to serialization such that this code doesn't need to build in
** silverlight.
===========================================================*/
namespace System.Collections.Generic
{
using System;
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.Serialization;
///
/// Used internally to control behavior of insertion into a .
///
internal enum InsertionBehavior : byte
{
///
/// The default insertion behavior.
///
None = 0,
///
/// Specifies that an existing entry with the same key should be overwritten if encountered.
///
OverwriteExisting = 1,
///
/// Specifies that if an existing entry with the same key is encountered, an exception should be thrown.
///
ThrowOnExisting = 2
}
[DebuggerTypeProxy(typeof(Mscorlib_DictionaryDebugView<,>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class Dictionary : IDictionary, IDictionary, IReadOnlyDictionary, ISerializable, IDeserializationCallback
{
private struct Entry
{
public int hashCode; // Lower 31 bits of hash code, -1 if unused
public int next; // Index of next entry, -1 if last
public TKey key; // Key of entry
public TValue value; // Value of entry
}
private int[] buckets;
private Entry[] entries;
private int count;
private int version;
private int freeList;
private int freeCount;
private IEqualityComparer comparer;
private KeyCollection keys;
private ValueCollection values;
private Object _syncRoot;
// constants for serialization
private const String VersionName = "Version";
private const String HashSizeName = "HashSize"; // Must save buckets.Length
private const String KeyValuePairsName = "KeyValuePairs";
private const String ComparerName = "Comparer";
public Dictionary() : this(0, null) { }
public Dictionary(int capacity) : this(capacity, null) { }
public Dictionary(IEqualityComparer comparer) : this(0, comparer) { }
public Dictionary(int capacity, IEqualityComparer comparer)
{
if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
if (capacity > 0) Initialize(capacity);
this.comparer = comparer ?? EqualityComparer.Default;
#if FEATURE_RANDOMIZED_STRING_HASHING
if (HashHelpers.s_UseRandomizedStringHashing && this.comparer == EqualityComparer.Default)
{
this.comparer = (IEqualityComparer)NonRandomizedStringEqualityComparer.Default;
}
#endif // FEATURE_RANDOMIZED_STRING_HASHING
}
public Dictionary(IDictionary dictionary) : this(dictionary, null) { }
public Dictionary(IDictionary dictionary, IEqualityComparer comparer) :
this(dictionary != null ? dictionary.Count : 0, comparer)
{
if (dictionary == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);
}
// It is likely that the passed-in dictionary is Dictionary. 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 and not a subclass, to maintain
// back-compat with subclasses that may have overridden the enumerator behavior.
if (dictionary.GetType() == typeof(Dictionary))
{
Dictionary d = (Dictionary)dictionary;
int count = d.count;
Entry[] entries = d.entries;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0)
{
Add(entries[i].key, entries[i].value);
}
}
return;
}
foreach (KeyValuePair pair in dictionary)
{
Add(pair.Key, pair.Value);
}
}
public Dictionary(IEnumerable> collection) :
this(collection, null)
{ }
public Dictionary(IEnumerable> collection, IEqualityComparer comparer) :
this((collection as ICollection>)?.Count ?? 0, comparer)
{
if (collection == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
}
foreach (KeyValuePair pair in collection)
{
Add(pair.Key, pair.Value);
}
}
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 Comparer
{
get
{
return comparer;
}
}
public int Count
{
get { return count - freeCount; }
}
public KeyCollection Keys
{
get
{
Contract.Ensures(Contract.Result() != null);
if (keys == null) keys = new KeyCollection(this);
return keys;
}
}
ICollection IDictionary.Keys
{
get
{
if (keys == null) keys = new KeyCollection(this);
return keys;
}
}
IEnumerable IReadOnlyDictionary.Keys
{
get
{
if (keys == null) keys = new KeyCollection(this);
return keys;
}
}
public ValueCollection Values
{
get
{
Contract.Ensures(Contract.Result() != null);
if (values == null) values = new ValueCollection(this);
return values;
}
}
ICollection IDictionary.Values
{
get
{
if (values == null) values = new ValueCollection(this);
return values;
}
}
IEnumerable IReadOnlyDictionary.Values
{
get
{
if (values == null) values = new ValueCollection(this);
return values;
}
}
public TValue this[TKey key]
{
get
{
int i = FindEntry(key);
if (i >= 0) return entries[i].value;
ThrowHelper.ThrowKeyNotFoundException();
return default(TValue);
}
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>.Add(KeyValuePair keyValuePair)
{
Add(keyValuePair.Key, keyValuePair.Value);
}
bool ICollection>.Contains(KeyValuePair keyValuePair)
{
int i = FindEntry(keyValuePair.Key);
if (i >= 0 && EqualityComparer.Default.Equals(entries[i].value, keyValuePair.Value))
{
return true;
}
return false;
}
bool ICollection>.Remove(KeyValuePair keyValuePair)
{
int i = FindEntry(keyValuePair.Key);
if (i >= 0 && EqualityComparer.Default.Equals(entries[i].value, keyValuePair.Value))
{
Remove(keyValuePair.Key);
return true;
}
return false;
}
public void Clear()
{
if (count > 0)
{
for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
Array.Clear(entries, 0, count);
freeList = -1;
count = 0;
freeCount = 0;
version++;
}
}
public bool ContainsKey(TKey key)
{
return FindEntry(key) >= 0;
}
public bool ContainsValue(TValue value)
{
if (value == null)
{
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0 && entries[i].value == null) return true;
}
}
else
{
EqualityComparer c = EqualityComparer.Default;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0 && c.Equals(entries[i].value, value)) return true;
}
}
return false;
}
private void CopyTo(KeyValuePair[] array, int index)
{
if (array == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
}
if (index < 0 || index > array.Length)
{
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
}
if (array.Length - index < Count)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
}
int count = this.count;
Entry[] entries = this.entries;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0)
{
array[index++] = new KeyValuePair(entries[i].key, entries[i].value);
}
}
}
public Enumerator GetEnumerator()
{
return new Enumerator(this, Enumerator.KeyValuePair);
}
IEnumerator> IEnumerable>.GetEnumerator()
{
return new Enumerator(this, Enumerator.KeyValuePair);
}
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));
info.AddValue(HashSizeName, buckets == null ? 0 : buckets.Length); //This is the length of the bucket array.
if (buckets != null)
{
KeyValuePair[] array = new KeyValuePair[Count];
CopyTo(array, 0);
info.AddValue(KeyValuePairsName, array, typeof(KeyValuePair[]));
}
}
private int FindEntry(TKey key)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null)
{
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
}
return -1;
}
private void Initialize(int capacity)
{
int size = HashHelpers.GetPrime(capacity);
buckets = new int[size];
for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
entries = new Entry[size];
freeList = -1;
}
private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets == null) Initialize(0);
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length;
#if FEATURE_RANDOMIZED_STRING_HASHING
int collisionCount = 0;
#endif
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
{
if (behavior == InsertionBehavior.OverwriteExisting)
{
entries[i].value = value;
version++;
return true;
}
if (behavior == InsertionBehavior.ThrowOnExisting)
{
ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(key);
}
return false;
}
#if FEATURE_RANDOMIZED_STRING_HASHING
collisionCount++;
#endif
}
int index;
if (freeCount > 0)
{
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else
{
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
index = count;
count++;
}
entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
buckets[targetBucket] = index;
version++;
#if FEATURE_RANDOMIZED_STRING_HASHING
// In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
// in this case will be EqualityComparer.Default.
// Note, randomized string hashing is turned on by default on coreclr so EqualityComparer.Default will
// be using randomized string hashing
if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default)
{
comparer = (IEqualityComparer)EqualityComparer.Default;
Resize(entries.Length, true);
}
#endif
return true;
}
public virtual void OnDeserialization(Object sender)
{
SerializationInfo siInfo;
HashHelpers.SerializationInfoTable.TryGetValue(this, out siInfo);
if (siInfo == null)
{
// It might be necessary to call OnDeserialization from a container if the container object also implements
// OnDeserialization. However, remoting will call OnDeserialization again.
// We can return immediately if this function is called twice.
// Note we set 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)siInfo.GetValue(ComparerName, typeof(IEqualityComparer));
if (hashsize != 0)
{
buckets = new int[hashsize];
for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
entries = new Entry[hashsize];
freeList = -1;
KeyValuePair[] array = (KeyValuePair[])
siInfo.GetValue(KeyValuePairsName, typeof(KeyValuePair[]));
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)
{
Debug.Assert(newSize >= entries.Length);
int[] newBuckets = new int[newSize];
for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
Entry[] newEntries = new Entry[newSize];
Array.Copy(entries, 0, newEntries, 0, count);
if (forceNewHashCodes)
{
for (int i = 0; i < count; i++)
{
if (newEntries[i].hashCode != -1)
{
newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
}
}
}
for (int i = 0; i < count; i++)
{
if (newEntries[i].hashCode >= 0)
{
int bucket = newEntries[i].hashCode % newSize;
newEntries[i].next = newBuckets[bucket];
newBuckets[bucket] = i;
}
}
buckets = newBuckets;
entries = newEntries;
}
// 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)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null)
{
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -1;
for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
{
if (last < 0)
{
buckets[bucket] = entries[i].next;
}
else
{
entries[last].next = entries[i].next;
}
entries[i].hashCode = -1;
entries[i].next = freeList;
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;
freeCount++;
version++;
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, out TValue value)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null)
{
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -1;
for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
{
if (last < 0)
{
buckets[bucket] = entries[i].next;
}
else
{
entries[last].next = entries[i].next;
}
value = entries[i].value;
entries[i].hashCode = -1;
entries[i].next = freeList;
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;
freeCount++;
version++;
return true;
}
}
}
value = default(TValue);
return false;
}
public bool TryGetValue(TKey key, out TValue value)
{
int i = FindEntry(key);
if (i >= 0)
{
value = entries[i].value;
return true;
}
value = default(TValue);
return false;
}
// Method similar to TryGetValue that returns the value instead of putting it in an out param.
public TValue GetValueOrDefault(TKey key) => GetValueOrDefault(key, default(TValue));
// Method similar to TryGetValue that returns the value instead of putting it in an out param. If the entry
// doesn't exist, returns the defaultValue instead.
public TValue GetValueOrDefault(TKey key, TValue defaultValue)
{
int i = FindEntry(key);
if (i >= 0)
{
return entries[i].value;
}
return defaultValue;
}
public bool TryAdd(TKey key, TValue value) => TryInsert(key, value, InsertionBehavior.None);
bool ICollection>.IsReadOnly
{
get { return false; }
}
void ICollection>.CopyTo(KeyValuePair[] 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 (index < 0 || index > array.Length)
{
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
}
if (array.Length - index < Count)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
}
KeyValuePair[] pairs = array as KeyValuePair[];
if (pairs != null)
{
CopyTo(pairs, index);
}
else if (array is DictionaryEntry[])
{
DictionaryEntry[] dictEntryArray = array as DictionaryEntry[];
Entry[] entries = this.entries;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0)
{
dictEntryArray[index++] = new DictionaryEntry(entries[i].key, entries[i].value);
}
}
}
else
{
object[] objects = array as object[];
if (objects == null)
{
ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType();
}
try
{
int count = this.count;
Entry[] entries = this.entries;
for (int i = 0; i < count; i++)
{
if (entries[i].hashCode >= 0)
{
objects[index++] = new KeyValuePair(entries[i].key, entries[i].value);
}
}
}
catch (ArrayTypeMismatchException)
{
ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType();
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(this, Enumerator.KeyValuePair);
}
bool ICollection.IsSynchronized
{
get { return false; }
}
object ICollection.SyncRoot
{
get
{
if (_syncRoot == null)
{
System.Threading.Interlocked.CompareExchange