using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Security.Permissions; using JD.Collections.Extensions; using JD.Kernel.StaticHelpers; namespace JD.Collections.Details { /// /// Stores values using a hasing algorithm. /// /// Should eventually replace Hash [Serializable, ComVisible(false), DebuggerDisplay("Count = {Count}")] public class FastHash : IHash, ISerializable, IDeserializationCallback, IVariableSizeHash { #region Constants private const int GrowFactor = 2; private const int InvalidIndex = 0; public const int MaxCount = int.MaxValue - 2; public const int MaxIndex = int.MaxValue - 1; private const int NonZeroMask = 0x00000001; // Make sure hashes are not 0, it means an empty hash. private const int PositiveMask = 0x7FFFFFFF; // Make sure hashes are not negatives or module returns negative indexes #endregion #region Variables private int[] m_table; private readonly IEqualityComparer m_comparer; private Entry[] m_entries; // Entries table, first slot is always empty so that 0 means invalid index --> removes the need to initialize the collection. private int m_nextFreeSlot = InvalidIndex; private int m_insertIndex = 1; private int m_freeCount; private int m_version; [NonSerialized] private SerializationInfo m_serializationInfo; #endregion #region Constructor public FastHash() : this(0, null) { } public FastHash(IEqualityComparer comparer) : this(0, comparer) { } public FastHash(int capacity) : this(capacity, null) { } public FastHash(int capacity, IEqualityComparer comparer) { Throw.ArgumentOutOfRangeIf(capacity < 0, "capacity", "capacity must be positive"); Initialize(capacity); m_comparer = comparer ?? EqualityComparer.Default; } protected FastHash(SerializationInfo serializationInfo, StreamingContext context) { m_insertIndex = 1; m_freeCount = 0; m_nextFreeSlot = InvalidIndex; m_serializationInfo = serializationInfo; m_comparer = (IEqualityComparer)m_serializationInfo.GetValue("Comparer", typeof(IEqualityComparer)); } protected FastHash(FastHash other) : this(other.Count, other.Comparer) { other.ForEach(Add); } #endregion #region Properties public IEqualityComparer Comparer { get { return m_comparer; } } public int Count { get { return m_insertIndex - 1 - m_freeCount; } } /// /// Get or set the count of items the hash can contain (cannot be less than the current size). /// public int Capacity { get { return (int)(m_entries.Length - 1); } set { if (value > Capacity) { ResizeHashTable(value); ResizeEntries(value + 1); } } } /// /// Enumerates the content of the hash but does not garantee to return elements added after the enumeration started. Garantees to not throw or not cause any threading issue. /// public IEnumerable ThreadSafeEnumerate { get { var values = m_entries; for (int i = 0; i < values.Length; i++) { var entry = values[i]; if (!entry.Empty) { yield return entry.Value; } } } } public T this[T key] { get { T result; TryGetValue(key, out result); return result; } } public int Version { get { return m_version; } } #endregion #region Public Methods /// /// Make sure current enumerations will crash on next MoveNext(). /// public void StopEnumerations() { m_version++; } public void TrimExcess() { if (m_entries.Length < m_insertIndex) { Entry[] entries = new Entry[m_insertIndex]; Array.Copy(m_entries, entries, m_insertIndex); m_entries = entries; } } public void Add(T value) { Insert(value, CollisionBehavior.Throw); } public void AddRange(IEnumerable values) { Capacity += values.Count(); foreach (var value in values) { Insert(value, CollisionBehavior.Throw); } } /// /// Adds a new item to the set. If the set already contains an item equal to /// , that item is replaced with . /// /// /// Equality between items is determined by the comparison instance or delegate used to create the set. /// Adding an item takes approximately constant time, regardless of the number of items in the set. /// The item to add to the set. /// True if it added the value (if it wasn't present). public bool TryAdd(T item) { return Insert(item, CollisionBehavior.Replace); } /// /// Adds a new item to the set. If the set already contains an item equal to /// , that item is replaced with . /// /// /// Equality between items is determined by the comparison instance or delegate used to create the set. /// Adding an item takes approximately constant time, regardless of the number of items in the set. /// The item to add to the set. If it was present, it will be the item in the hash. /// True if it added the value (if it wasn't present). public bool TryAdd(ref T item) { return Insert(ref item, CollisionBehavior.KeepActual); } public void Clear() { if (m_insertIndex > 1) { Array.Clear(m_table, 0, m_table.Length); Array.Clear(m_entries, 0, m_insertIndex); m_nextFreeSlot = InvalidIndex; m_insertIndex = 1; m_freeCount = 0; m_version++; } } public bool Remove(T value) { int hash; unchecked { hash = (int)((m_comparer.GetSupportingNullHashCode(value) | NonZeroMask) & PositiveMask); } int startIndex = hash % m_table.Length; int previousIndex = InvalidIndex; for (var index = m_table[startIndex]; index != InvalidIndex; index = m_entries[index].Next) { if (m_entries[index].HashCode == hash && m_comparer.Equals(m_entries[index].Value, value)) { if (previousIndex != InvalidIndex) { m_entries[previousIndex].Next = m_entries[index].Next; } else { // Removing first entry of list. m_table[startIndex] = m_entries[index].Next; } m_entries[index] = new Entry { Next = m_nextFreeSlot }; m_nextFreeSlot = index; m_freeCount++; m_version++; return true; } previousIndex = index; } return false; } public bool Remove(ILookup lookup, out T removedValue) { int hash; unchecked { hash = (int)((lookup.HashCode | NonZeroMask) & PositiveMask); } int startIndex = hash % m_table.Length; int previousIndex = InvalidIndex; for (var index = m_table[startIndex]; index != InvalidIndex; index = m_entries[index].Next) { if (m_entries[index].HashCode == hash && lookup.Equals(m_entries[index].Value)) { if (previousIndex != InvalidIndex) { m_entries[previousIndex].Next = m_entries[index].Next; } else { // Removing first entry of list. m_table[startIndex] = m_entries[index].Next; } removedValue = m_entries[index].Value; m_entries[index].Next = m_nextFreeSlot; m_entries[index].HashCode = InvalidIndex; m_entries[index].Value = default(T); m_nextFreeSlot = index; m_freeCount++; m_version++; return true; } previousIndex = index; } removedValue = default(T); return false; } public bool Contains(T value) { int hash; unchecked { hash = (int)((m_comparer.GetSupportingNullHashCode(value) | NonZeroMask) & PositiveMask); } for (var i = m_table[hash % m_table.Length]; i != InvalidIndex; i = m_entries[i].Next) { if ((m_entries[i].HashCode == hash) && m_comparer.Equals(m_entries[i].Value, value)) { return true; } } return false; } public bool ContainsKey(ILookup lookup) { return FindEntry(lookup) != InvalidIndex; } public bool TryGetValue(T value, out T result) { int index = FindEntry(value); if (index != InvalidIndex) { result = m_entries[index].Value; return true; } result = default(T); return false; } public bool TryGetValue(ILookup lookup, out T result) { int index = FindEntry(lookup); if (index != InvalidIndex) { result = m_entries[index].Value; return true; } result = default(T); return false; } public FastHash Clone() { return new FastHash(this); } IHash IHash.Clone() { return Clone(); } /// /// Enumerates all the values that shares the hashcode that fits the lookup (useful for multidictionary). /// public IEnumerable EnumerateValues(ILookup lookup) { Throw.IfNull(lookup, "lookup"); int hash; unchecked { hash = (int)((lookup.HashCode | NonZeroMask) & PositiveMask); } for (var i = m_table[hash % m_table.Length]; i != InvalidIndex; i = m_entries[i].Next) { var value = m_entries[i].Value; if (lookup.Equals(value)) { yield return value; } } } #endregion #region Private Methods /// /// Not Using the Find with lookup to remove the overhead of creating an object for each search. /// private int FindEntry(T value) { int hash; unchecked { hash = (int)((m_comparer.GetSupportingNullHashCode(value) | NonZeroMask) & PositiveMask); } for (var i = m_table[hash % m_table.Length]; i != InvalidIndex; i = m_entries[i].Next) { if ((m_entries[i].HashCode == hash) && m_comparer.Equals(m_entries[i].Value, value)) { return i; } } return InvalidIndex; } private int FindEntry(ILookup lookup) { Throw.IfNull(lookup, "lookup"); int hash; unchecked { hash = (int)((lookup.HashCode | NonZeroMask) & PositiveMask); } for (var i = m_table[hash % m_table.Length]; i != InvalidIndex; i = m_entries[i].Next) { if ((m_entries[i].HashCode == hash) && lookup.Equals(m_entries[i].Value)) { return i; } } return InvalidIndex; } private void Initialize(int capacity) { int prime = HashHelpers.GetPrime(capacity); m_table = new int[prime]; for (int i = 0; i < m_table.Length; i++) { m_table[i] = InvalidIndex; } m_entries = new Entry[Math.Min(Math.Max(2, capacity), MaxCount + 1)]; InitEntries(m_entries, 0); m_nextFreeSlot = InvalidIndex; } private static void InitEntries(Entry[] entries, int startIndex) { int length = entries.Length; for (int i = startIndex; i < length; i++) { entries[i].HashCode = InvalidIndex; entries[i].Next = InvalidIndex; } } private bool Insert(T value, CollisionBehavior collisionBehavior) { return Insert(ref value, collisionBehavior); } private bool Insert(ref T value, CollisionBehavior collisionBehavior) { if (Count + 1 > m_table.Length) { ResizeHashTable(m_table.Length * GrowFactor); } int hash; unchecked { hash = (int)((m_comparer.GetSupportingNullHashCode(value) | NonZeroMask) & PositiveMask); } int startIndex = hash % m_table.Length; int lastIndex = InvalidIndex; for (var index = m_table[startIndex]; index != InvalidIndex; index = m_entries[index].Next) { if (m_entries[index].Is(hash, value, m_comparer)) { if (collisionBehavior == CollisionBehavior.Throw) { throw new ArgumentException($"Item {value} is already present in dictionary", "value"); } else if (collisionBehavior == CollisionBehavior.Replace) { m_entries[index].Value = value; m_version++; } else if (collisionBehavior == CollisionBehavior.KeepActual) { value = m_entries[index].Value; } return false; } lastIndex = index; } // Found a collision if (lastIndex != InvalidIndex) { var index = InsertEntry(value, hash); m_entries[lastIndex].Next = index; } else { m_table[startIndex] = InsertEntry(value, hash); } return true; } private int InsertEntry(T value, int hash) { m_version++; int index; if (m_nextFreeSlot != InvalidIndex) { index = m_nextFreeSlot; m_nextFreeSlot = m_entries[index].Next; m_freeCount--; } else { if (m_insertIndex == MaxIndex) { throw new InvalidOperationException("Hash has reach its maximum element count"); } if (m_entries.Length <= m_insertIndex) { ResizeEntries(); } index = m_insertIndex; m_insertIndex++; } m_entries[index].HashCode = hash; m_entries[index].Value = value; m_entries[index].Next = InvalidIndex; return index; } private void ResizeHashTable(int expectedSize) { int prime = HashHelpers.GetPrime(expectedSize); var newTable = new int[prime]; for (int i = 0; i < m_entries.Length; i++) { if (!m_entries[i].Empty) { int insertIndex = m_entries[i].HashCode % prime; m_entries[i].Next = newTable[insertIndex]; newTable[insertIndex] = i; } else { m_entries[i].Next = InvalidIndex; } } m_table = newTable; } private void ResizeEntries() { ResizeEntries(m_entries.Length * 2); } private void ResizeEntries(int entryCount) { Entry[] entries = new Entry[Math.Min(entryCount, MaxCount + 1)]; Array.Copy(m_entries, entries, m_insertIndex); m_entries = entries; } #endregion #region Serialization [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { Throw.IfNull(info, "info"); info.AddValue("Comparer", m_comparer); info.AddValue("Values", this.ToArray()); } public void OnDeserialization(object sender) { if (m_serializationInfo != null) { T[] values = (T[])m_serializationInfo.GetValue("Values", typeof(T[])); Initialize(values.Length); foreach (T value in values) { Add(value); } m_serializationInfo = null; } } #endregion #region Implementation of IEnumerable public IEnumerator GetEnumerator() { int startVersion = m_version; int index = 1; while (index < m_insertIndex) { if (startVersion != m_version) { throw new InvalidOperationException("Collection was modified during the enumeration"); } if (!m_entries[index].Empty) { yield return m_entries[index].Value; } index++; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion #region Nested [StructLayout(LayoutKind.Sequential), Serializable] private struct Entry { public int HashCode; public int Next; public T Value; public bool Empty { get { return HashCode == InvalidIndex; } } public bool Is(int hash, T value, IEqualityComparer comparer) { return HashCode == hash && comparer.Equals(Value, value); } } public static class Invariants { public static void Validate(FastHash hash) { ValidateCount(hash); ValidateHash(hash); ValidateFreeList(hash); } private static void ValidateCount(FastHash hash) { int expectedCount = 0; for (int i = 0; i < hash.m_entries.Length; i++) { if (!hash.m_entries[i].Empty) { expectedCount++; } } Throw.InvalidProgramIf(expectedCount != hash.Count, "Invalid count. Expected {0}, was {1}", expectedCount, hash.Count); } private static void ValidateFreeList(FastHash hash) { int expectedFreeCount = 0; for (int i = hash.m_nextFreeSlot; i != InvalidIndex; i = hash.m_entries[i].Next) { expectedFreeCount++; } Throw.InvalidProgramIf(expectedFreeCount != hash.m_freeCount, "Invalid free list"); } private static void ValidateHash(FastHash hash) { int reachable = 0; for (int i = 0; i < hash.m_table.Length; i++) { // Validate each bucket for (int node = hash.m_table[i]; node != InvalidIndex; node = hash.m_entries[node].Next) { Entry entry = hash.m_entries[node]; int expectedHash; unchecked { expectedHash = (int)((hash.m_comparer.GetSupportingNullHashCode(entry.Value) | NonZeroMask) & PositiveMask); } Throw.InvalidProgramIf(entry.HashCode != expectedHash, "Invalid hash"); int expectedBucket = entry.HashCode % hash.m_table.Length; Throw.InvalidProgramIf(expectedBucket != i, "Invalid hash"); Throw.InvalidProgramIf(entry.Empty, "Invalid hash"); reachable++; } } Throw.InvalidProgramIf(reachable != hash.Count, "Invalid entry list"); } } #endregion // IVariableHash is not implemented since for now, 2G items is plenty enough! bool IVariableSizeHash.Add(T value, CollisionBehavior collisionBehavior, ref IVariableSizeHash newHash) { return Insert(value, collisionBehavior); } void IVariableSizeHash.AddRange(IEnumerable values, ref IVariableSizeHash newHash) { AddRange(values); } IVariableSizeHash IVariableSizeHash.Clone() { return Clone(); } } }