749 lines
No EOL
23 KiB
C#
749 lines
No EOL
23 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Stores values using a hasing algorithm.
|
|
/// </summary>
|
|
/// <remarks>Should eventually replace Hash</remarks>
|
|
[Serializable, ComVisible(false), DebuggerDisplay("Count = {Count}")]
|
|
public class FastHash<T> : IHash<T>, ISerializable, IDeserializationCallback, IVariableSizeHash<T>
|
|
{
|
|
#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<T> 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<T> comparer)
|
|
: this(0, comparer)
|
|
{
|
|
}
|
|
|
|
public FastHash(int capacity)
|
|
: this(capacity, null)
|
|
{
|
|
}
|
|
|
|
public FastHash(int capacity, IEqualityComparer<T> comparer)
|
|
{
|
|
Throw.ArgumentOutOfRangeIf(capacity < 0, "capacity", "capacity must be positive");
|
|
|
|
Initialize(capacity);
|
|
|
|
m_comparer = comparer ?? EqualityComparer<T>.Default;
|
|
}
|
|
|
|
protected FastHash(SerializationInfo serializationInfo, StreamingContext context)
|
|
{
|
|
m_insertIndex = 1;
|
|
m_freeCount = 0;
|
|
m_nextFreeSlot = InvalidIndex;
|
|
m_serializationInfo = serializationInfo;
|
|
m_comparer = (IEqualityComparer<T>)m_serializationInfo.GetValue("Comparer", typeof(IEqualityComparer<T>));
|
|
}
|
|
|
|
protected FastHash(FastHash<T> other)
|
|
: this(other.Count, other.Comparer)
|
|
{
|
|
other.ForEach(Add);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public IEqualityComparer<T> Comparer
|
|
{
|
|
get
|
|
{
|
|
return m_comparer;
|
|
}
|
|
}
|
|
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
return m_insertIndex - 1 - m_freeCount;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or set the count of items the hash can contain (cannot be less than the current size).
|
|
/// </summary>
|
|
public int Capacity
|
|
{
|
|
get { return (int)(m_entries.Length - 1); }
|
|
set
|
|
{
|
|
if (value > Capacity)
|
|
{
|
|
ResizeHashTable(value);
|
|
ResizeEntries(value + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public IEnumerable<T> 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
|
|
|
|
/// <summary>
|
|
/// Make sure current enumerations will crash on next MoveNext().
|
|
/// </summary>
|
|
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<T> values)
|
|
{
|
|
Capacity += values.Count();
|
|
foreach (var value in values)
|
|
{
|
|
Insert(value, CollisionBehavior.Throw);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new item to the set. If the set already contains an item equal to
|
|
/// <paramref name="item"/>, that item is replaced with <paramref name="item"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Equality between items is determined by the comparison instance or delegate used to create the set.</para>
|
|
/// <para>Adding an item takes approximately constant time, regardless of the number of items in the set.</para></remarks>
|
|
/// <param name="item">The item to add to the set.</param>
|
|
/// <returns>True if it added the value (if it wasn't present).</returns>
|
|
public bool TryAdd(T item)
|
|
{
|
|
return Insert(item, CollisionBehavior.Replace);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new item to the set. If the set already contains an item equal to
|
|
/// <paramref name="item"/>, that item is replaced with <paramref name="item"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Equality between items is determined by the comparison instance or delegate used to create the set.</para>
|
|
/// <para>Adding an item takes approximately constant time, regardless of the number of items in the set.</para></remarks>
|
|
/// <param name="item">The item to add to the set. If it was present, it will be the item in the hash.</param>
|
|
/// <returns>True if it added the value (if it wasn't present).</returns>
|
|
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<T> 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<T> 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<T> 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<T> Clone()
|
|
{
|
|
return new FastHash<T>(this);
|
|
}
|
|
|
|
IHash<T> IHash<T>.Clone()
|
|
{
|
|
return Clone();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates all the values that shares the hashcode that fits the lookup (useful for multidictionary).
|
|
/// </summary>
|
|
public IEnumerable<T> EnumerateValues(ILookup<T> 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
|
|
|
|
/// <summary>
|
|
/// Not Using the Find with lookup to remove the overhead of creating an object for each search.
|
|
/// </summary>
|
|
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<T> 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<T> 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<T> comparer)
|
|
{
|
|
return HashCode == hash && comparer.Equals(Value, value);
|
|
}
|
|
}
|
|
|
|
public static class Invariants
|
|
{
|
|
public static void Validate(FastHash<T> hash)
|
|
{
|
|
ValidateCount(hash);
|
|
ValidateHash(hash);
|
|
ValidateFreeList(hash);
|
|
}
|
|
|
|
private static void ValidateCount(FastHash<T> 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<T> 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<T> 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<T>.Add(T value, CollisionBehavior collisionBehavior, ref IVariableSizeHash<T> newHash)
|
|
{
|
|
return Insert(value, collisionBehavior);
|
|
}
|
|
|
|
void IVariableSizeHash<T>.AddRange(IEnumerable<T> values, ref IVariableSizeHash<T> newHash)
|
|
{
|
|
AddRange(values);
|
|
}
|
|
|
|
IVariableSizeHash<T> IVariableSizeHash<T>.Clone()
|
|
{
|
|
return Clone();
|
|
}
|
|
}
|
|
} |