diff --git a/ICD.Common.Utils.Tests/Collections/WeakKeyDictionaryTest.cs b/ICD.Common.Utils.Tests/Collections/WeakKeyDictionaryTest.cs new file mode 100644 index 0000000..21c7874 --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/WeakKeyDictionaryTest.cs @@ -0,0 +1,158 @@ +using System; +using ICD.Common.Utils.Collections; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests.Collections +{ + [TestFixture] + public sealed class WeakKeyDictionaryTest + { + private sealed class TestClass + { + } + + #region Properties + + [Test] + public void CountTest() + { +#if DEBUG + Assert.Inconclusive(); + return; +#endif + WeakKeyDictionary dict = new WeakKeyDictionary(); + Assert.AreEqual(0, dict.Count); + + TestClass instance = new TestClass(); + + dict.Add(instance, 0); + + Assert.AreEqual(1, dict.Count, "Expected the added item to increase the dictionary size"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.AreEqual(1, dict.Count, "Expected the dictionary to have one uncollected item"); + + // Need to actually USE the instance at some point AFTER the collection, otherwise it gets optimized out + // and is collected prematurely. + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + instance.ToString(); + + // Now clear the reference to make sure the instance gets collected. + // ReSharper disable once RedundantAssignment + instance = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.AreEqual(0, dict.Count, "Expected the dictionary to be empty after collecting"); + } + + [Test] + public void KeysTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ValuesTest() + { + Assert.Inconclusive(); + } + + [Test] + public void IndexerTest() + { + Assert.Inconclusive(); + } + +#endregion + +#region Methods + + [Test] + public void AddTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ContainsKeyTest() + { + Assert.Inconclusive(); + } + + [Test] + public void RemoveTest() + { + Assert.Inconclusive(); + } + + [Test] + public void TryGetValueTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ClearTest() + { + Assert.Inconclusive(); + } + + [Test] + public void RemoveCollectedEntries() + { +#if DEBUG + Assert.Inconclusive(); + return; +#endif + WeakKeyDictionary dict = new WeakKeyDictionary(); + + dict.RemoveCollectedEntries(); + Assert.AreEqual(0, dict.Count, "Expected new dictionary to start empty"); + + TestClass instance = new TestClass(); + + dict.Add(instance, 0); + dict.RemoveCollectedEntries(); + + Assert.AreEqual(1, dict.Count, "Expected instance to add to the dictionary count"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + dict.RemoveCollectedEntries(); + + Assert.AreEqual(1, dict.Count, "Expected instance to remain uncollected and stay in the dictionary"); + + // Need to actually USE the instance at some point AFTER the collection, otherwise it gets optimized out + // and is collected prematurely. + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + instance.ToString(); + + // Now clear the reference to make sure the instance gets collected. + // ReSharper disable once RedundantAssignment + instance = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + dict.RemoveCollectedEntries(); + + Assert.AreEqual(0, dict.Count, "Expected instance to be collected and removed from the dictionary"); + } + + [Test] + public void GetEnumeratorTest() + { + Assert.Inconclusive(); + } + +#endregion + } +} diff --git a/ICD.Common.Utils/Collections/WeakKeyDictionary.cs b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs new file mode 100644 index 0000000..fbc541a --- /dev/null +++ b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Properties; +using ICD.Common.Utils.Extensions; + +namespace ICD.Common.Utils.Collections +{ + public sealed class WeakKeyReference : WeakReference + { + private readonly int m_HashCode; + + public new T Target { get { return (T)base.Target; } } + + public int HashCode { get { return m_HashCode; } } + + /// + /// Constructor. + /// + /// + public WeakKeyReference(T key) + : this(key, EqualityComparer.Default) + { + } + + /// + /// Constructor. + /// + /// + /// + public WeakKeyReference(T key, IEqualityComparer comparer) + : base(key) + { + if (comparer == null) + throw new ArgumentNullException("comparer"); + + // Retain the object's hash code immediately so that even + // if the target is GC'ed we will be able to find and + // remove the dead weak reference. + m_HashCode = comparer.GetHashCode(key); + } + } + + public sealed class WeakKeyComparer : IEqualityComparer> + { + private readonly IEqualityComparer m_Comparer; + + /// + /// Constructor. + /// + /// + public WeakKeyComparer(IEqualityComparer comparer) + { + if (comparer == null) + comparer = EqualityComparer.Default; + + m_Comparer = comparer; + } + + public int GetHashCode(WeakKeyReference weakKey) + { + return weakKey.HashCode; + } + + // Note: There are actually 9 cases to handle here. + // + // Let Wa = Alive Weak Reference + // Let Wd = Dead Weak Reference + // Let S = Strong Reference + // + // x | y | Equals(x,y) + // ------------------------------------------------- + // Wa | Wa | comparer.Equals(x.Target, y.Target) + // Wa | Wd | false + // Wa | S | comparer.Equals(x.Target, y) + // Wd | Wa | false + // Wd | Wd | x == y + // Wd | S | false + // S | Wa | comparer.Equals(x, y.Target) + // S | Wd | false + // S | S | comparer.Equals(x, y) + // ------------------------------------------------- + public bool Equals(WeakKeyReference x, WeakKeyReference y) + { + bool xIsDead; + bool yIsDead; + + T first = GetTarget(x, out xIsDead); + T second = GetTarget(y, out yIsDead); + + if (xIsDead) + return yIsDead && x == y; + + return !yIsDead && m_Comparer.Equals(first, second); + } + + private static T GetTarget(WeakKeyReference obj, out bool isDead) + { + T target = obj.Target; + isDead = !obj.IsAlive; + return target; + } + } + + + /// + /// WeakDictionary keeps weak references to keys and drops key/value pairs once the key is garbage collected. + /// + /// + /// + public sealed class WeakKeyDictionary : IDictionary + { + private readonly Dictionary, TValue> m_Dictionary; + private readonly IEqualityComparer m_Comparer; + + #region Properties + + public int Count + { + get + { + RemoveCollectedEntries(); + return m_Dictionary.Count; + } + } + + public bool IsReadOnly { get { return false; } } + + public ICollection Keys { get { throw new NotImplementedException(); } } + + public ICollection Values { get { throw new NotImplementedException(); } } + + public TValue this[TKey key] + { + get + { + TValue output; + if (TryGetValue(key, out output)) + return output; + + throw new KeyNotFoundException(); + } + set + { + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + m_Dictionary[weakKey] = value; + } + } + + #endregion + + public WeakKeyDictionary() + : this(0) + { + } + + public WeakKeyDictionary(int capacity) + : this(capacity, EqualityComparer.Default) + { + } + + public WeakKeyDictionary(IEqualityComparer comparer) + : this(0, comparer) + { + } + + public WeakKeyDictionary(int capacity, IEqualityComparer comparer) + { + m_Comparer = comparer; + + WeakKeyComparer keyComparer = new WeakKeyComparer(m_Comparer); + m_Dictionary = new Dictionary, TValue>(capacity, keyComparer); + } + + #region Methods + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void Add(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + IcdConsole.PrintLine("Adding key {0} with hash code {1}", weakKey.Target, weakKey.HashCode); + + m_Dictionary.Add(weakKey, value); + + IcdConsole.PrintLine("Internal dict count is now {0}", m_Dictionary.Count); + } + + public bool ContainsKey(TKey key) + { + TValue unused; + return TryGetValue(key, out unused); + } + + public bool Remove(TKey key) + { + return m_Dictionary.Remove(new WeakKeyReference(key, m_Comparer)); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return m_Dictionary.TryGetValue(new WeakKeyReference(key, m_Comparer), out value); + } + + public void Clear() + { + m_Dictionary.Clear(); + } + + /// + /// Removes the left-over weak references for entries in the dictionary + /// whose key or value has already been reclaimed by the garbage + /// collector. This will reduce the dictionary's Count by the number + /// of dead key-value pairs that were eliminated. + /// + [PublicAPI] + public void RemoveCollectedEntries() + { + IEnumerable> toRemove = + m_Dictionary.Select(pair => pair.Key) + .Where(weakKey => !weakKey.IsAlive) + .ToArray(); + + IcdConsole.PrintLine("-------------------"); + foreach (var item in toRemove) + IcdConsole.PrintLine("{0} - {1}", item, item.Target); + IcdConsole.PrintLine("-------------------"); + + m_Dictionary.RemoveAll(toRemove); + } + + #region ICollection + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + #endregion + + #region IEnumerator + + public IEnumerator> GetEnumerator() + { + foreach (KeyValuePair, TValue> kvp in m_Dictionary) + { + WeakKeyReference weakKey = kvp.Key; + TKey key = weakKey.Target; + if (weakKey.IsAlive) + yield return new KeyValuePair(key, kvp.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #endregion + } +} diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 8038e0b..f69e8d2 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -74,6 +74,7 @@ +