diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from CHANGELOG.txt rename to CHANGELOG.md 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..d8fc681 --- /dev/null +++ b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs @@ -0,0 +1,271 @@ +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 { return GetAliveKvps().Count(); } } + + public bool IsReadOnly { get { return false; } } + + public ICollection Keys { get { return GetAliveKvps().Select(kvp => kvp.Key).ToArray(); } } + + public ICollection Values { get { return GetAliveKvps().Select(kvp => kvp.Value).ToArray(); } } + + 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 + + public void Add(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + m_Dictionary.Add(weakKey, value); + } + + 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(); + + m_Dictionary.RemoveAll(toRemove); + } + + #region ICollection + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ContainsKey(item.Key); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + GetAliveKvps().CopyTo(array, arrayIndex); + } + + private IEnumerable> GetAliveKvps() + { + 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); + } + } + + #endregion + + #region IEnumerator + + public IEnumerator> GetEnumerator() + { + return GetAliveKvps().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #endregion + } +} diff --git a/ICD.Common.Utils/Extensions/EnumerableExtensions.cs b/ICD.Common.Utils/Extensions/EnumerableExtensions.cs index 74e69ba..865d2ee 100644 --- a/ICD.Common.Utils/Extensions/EnumerableExtensions.cs +++ b/ICD.Common.Utils/Extensions/EnumerableExtensions.cs @@ -578,6 +578,23 @@ namespace ICD.Common.Utils.Extensions return extends.Where(i => !comparer.Equals(item, i)); } + /// + /// Copies all the elements of the current one-dimensional array to the specified one-dimensional array + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// + /// + /// + /// + /// + public static void CopyTo(this IEnumerable extends, T[] array, int index) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + ICollection collection = extends as ICollection ?? extends.ToArray(); + collection.CopyTo(array, index); + } + /// /// Returns the sequence as a IcdHashSet. /// diff --git a/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs b/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs index cb812b7..6e222db 100644 --- a/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs +++ b/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs @@ -14,9 +14,6 @@ namespace ICD.Common.Utils.Extensions /// public static void Raise(this EventHandler extends, object sender) { - if (sender == null) - throw new ArgumentNullException("sender"); - if (extends != null) extends(sender, EventArgs.Empty); } @@ -31,9 +28,6 @@ namespace ICD.Common.Utils.Extensions public static void Raise(this EventHandler extends, object sender, T args) where T : EventArgs { - if (sender == null) - throw new ArgumentNullException("sender"); - if (args == null) throw new ArgumentNullException("args"); diff --git a/ICD.Common.Utils/Extensions/JsonExtensions.cs b/ICD.Common.Utils/Extensions/JsonExtensions.cs index 77a0eb0..ac56fa2 100644 --- a/ICD.Common.Utils/Extensions/JsonExtensions.cs +++ b/ICD.Common.Utils/Extensions/JsonExtensions.cs @@ -75,6 +75,24 @@ namespace ICD.Common.Utils.Extensions return Type.GetType(value); } + /// + /// Gets the current value as an unsigned integer. + /// + /// + /// + [PublicAPI] + public static uint GetValueAsUInt(this JsonReader extends) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (extends.TokenType == JsonToken.Integer) + return (uint)(long)extends.Value; + + string message = string.Format("Token {0} {1} is not {2}", extends.TokenType, extends.Value, JsonToken.Integer); + throw new InvalidCastException(message); + } + /// /// Gets the current value as an integer. /// diff --git a/ICD.Common.Utils/Extensions/ListExtensions.cs b/ICD.Common.Utils/Extensions/ListExtensions.cs index 9e72772..e5394a8 100644 --- a/ICD.Common.Utils/Extensions/ListExtensions.cs +++ b/ICD.Common.Utils/Extensions/ListExtensions.cs @@ -10,6 +10,46 @@ namespace ICD.Common.Utils.Extensions /// public static class ListExtensions { + /// + /// Adds the items into a sorted list. + /// + /// + /// + /// + [PublicAPI] + public static void AddSorted(this List extends, IEnumerable items) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (items == null) + throw new ArgumentNullException("items"); + + extends.AddSorted(items, Comparer.Default); + } + + /// + /// Adds the item into a sorted list. + /// + /// + /// + /// + /// + [PublicAPI] + public static void AddSorted(this List extends, IEnumerable items, IComparer comparer) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (items == null) + throw new ArgumentNullException("items"); + + if (comparer == null) + throw new ArgumentNullException("comparer"); + + items.ForEach(i => extends.AddSorted(i, comparer)); + } + /// /// Adds the item into a sorted list. /// 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 @@ + diff --git a/ICD.Common.Utils/ReflectionUtils.cs b/ICD.Common.Utils/ReflectionUtils.cs index 1260bb6..534ca34 100644 --- a/ICD.Common.Utils/ReflectionUtils.cs +++ b/ICD.Common.Utils/ReflectionUtils.cs @@ -414,5 +414,43 @@ namespace ICD.Common.Utils throw new InvalidCastException(message, e); } } + + /// + /// Subscribes to the event on the given instance using the handler and callback method. + /// + /// The instance with the event. Null for static types. + /// The EventInfo for the event. + /// The instance with the callback MethodInfo. Null for static types. + /// The MethodInfo for the callback method. + /// + public static Delegate SubscribeEvent(object instance, EventInfo eventInfo, object handler, MethodInfo callback) + { + if (eventInfo == null) + throw new ArgumentNullException("eventInfo"); + + if (callback == null) + throw new ArgumentNullException("callback"); + + Delegate output = CreateDelegate(eventInfo.EventHandlerType, handler, callback); + eventInfo.AddEventHandler(instance, output); + return output; + } + + /// + /// Unsubscribes from the event on the given instance. + /// + /// The instance with the event. Null for static types. + /// The EventInfo for the event. + /// The Delegate to be removed from the event. + public static void UnsubscribeEvent(object instance, EventInfo eventInfo, Delegate callback) + { + if (eventInfo == null) + throw new ArgumentNullException("eventInfo"); + + if (callback == null) + throw new ArgumentNullException("callback"); + + eventInfo.RemoveEventHandler(instance, callback); + } } }