KeyedList.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. namespace Animancer
  6. {
  7. /// <summary>Stores the index of an object in a <see cref="KeyedList{T}"/>.</summary>
  8. /// https://kybernetik.com.au/animancer/api/Animancer/Key
  9. ///
  10. public class Key : Key.IListItem
  11. {
  12. /************************************************************************************************************************/
  13. /// <summary>An object with a <see cref="Animancer.Key"/> so it can be used in a <see cref="Key.KeyedList{T}"/>.</summary>
  14. /// <example>
  15. /// It is usually easiest to just inherit from <see cref="Animancer.Key"/>, but if that is not possible then the
  16. /// recommended implementation looks like this:
  17. /// <para></para><code>
  18. /// class MyClass : Key.IListItem
  19. /// {
  20. /// private readonly Key Key = new Key();
  21. /// Key Key.IListItem.Key => Key;
  22. /// }
  23. /// </code></example>
  24. /// https://kybernetik.com.au/animancer/api/Animancer/IListItem
  25. ///
  26. public interface IListItem
  27. {
  28. /// <summary>The <see cref="Animancer.Key"/> which stores the <see cref="KeyedList{T}"/> index of this object.</summary>
  29. Key Key { get; }
  30. }
  31. /************************************************************************************************************************/
  32. /// <summary>The <see cref="_Index"/> which indicates that an item isn't in a list.</summary>
  33. public const int NotInList = -1;
  34. /// <summary>The current position of this key in the list.</summary>
  35. private int _Index = -1;
  36. /// <summary>Returns location of this object in the list (or <c>-1</c> if it is not currently in a keyed list).</summary>
  37. public static int IndexOf(Key key) => key._Index;
  38. /// <summary>Is the `key` currently in a keyed list?</summary>
  39. public static bool IsInList(Key key) => key._Index != NotInList;
  40. /************************************************************************************************************************/
  41. /// <summary>A <see cref="Key"/> is its own <see cref="IListItem.Key"/>.</summary>
  42. Key IListItem.Key => this;
  43. /************************************************************************************************************************/
  44. /// <summary>A <see cref="List{T}"/> which can remove items without needing to search the entire collection.</summary>
  45. /// <remarks>
  46. /// This implementation has several restrictions compared to a regular <see cref="List{T}"/>:
  47. /// <list type="bullet">
  48. /// <item>Items must implement <see cref="IListItem"/> or inherit from <see cref="Key"/>.</item>
  49. /// <item>Items cannot be <c>null</c>.</item>
  50. /// <item>Items can only be in one <see cref="KeyedList{T}"/> at a time and cannot appear multiple times in it.</item>
  51. /// </list>
  52. /// This class is nested inside <see cref="Key"/> so it can modify the private <see cref="_Index"/> without
  53. /// exposing that capability to anything else.
  54. /// </remarks>
  55. /// https://kybernetik.com.au/animancer/api/Animancer/KeyedList_1
  56. ///
  57. public class KeyedList<T> : IList<T>, ICollection where T : class, IListItem
  58. {
  59. /************************************************************************************************************************/
  60. private const string
  61. SingleUse = "Each item can only be used in one " + nameof(KeyedList<T>) + " at a time.",
  62. NotFound = "The specified item does not exist in this " + nameof(KeyedList<T>) + ".";
  63. /************************************************************************************************************************/
  64. private readonly List<T> Items;
  65. /************************************************************************************************************************/
  66. /// <summary>Creates a new <see cref="KeyedList{T}"/> using the default <see cref="List{T}"/> constructor.</summary>
  67. public KeyedList() => Items = new List<T>();
  68. /// <summary>Creates a new <see cref="KeyedList{T}"/> with the specified initial `capacity`.</summary>
  69. public KeyedList(int capacity) => Items = new List<T>(capacity);
  70. // No copy constructor because the keys will not work if they are used in multiple lists at once.
  71. /************************************************************************************************************************/
  72. /// <summary>The number of items currently in the list.</summary>
  73. public int Count => Items.Count;
  74. /// <summary>The number of items that this list can contain before resizing is required.</summary>
  75. public int Capacity
  76. {
  77. get => Items.Capacity;
  78. set => Items.Capacity = value;
  79. }
  80. /************************************************************************************************************************/
  81. /// <summary>The item at the specified `index`.</summary>
  82. /// <exception cref="ArgumentException">The `value` was already in a keyed list (setter only).</exception>
  83. public T this[int index]
  84. {
  85. get => Items[index];
  86. set
  87. {
  88. var key = value.Key;
  89. // Make sure it isn't already in a list.
  90. if (key._Index != NotInList)
  91. throw new ArgumentException(SingleUse);
  92. // Remove the old item at that index.
  93. Items[index].Key._Index = NotInList;
  94. // Set the index of the new item and add it at that index.
  95. key._Index = index;
  96. Items[index] = value;
  97. }
  98. }
  99. /************************************************************************************************************************/
  100. /// <summary>Indicates whether the `item` is currently in this list.</summary>
  101. public bool Contains(T item)
  102. {
  103. if (item == null)
  104. return false;
  105. var index = item.Key._Index;
  106. return
  107. (uint)index < (uint)Items.Count &&
  108. Items[index] == item;
  109. }
  110. /************************************************************************************************************************/
  111. /// <summary>Returns the index of the `item` in this list or <c>-1</c> if it is not in this list.</summary>
  112. public int IndexOf(T item)
  113. {
  114. if (item == null)
  115. return NotInList;
  116. var index = item.Key._Index;
  117. if ((uint)index < (uint)Items.Count &&
  118. Items[index] == item)
  119. return index;
  120. else
  121. return NotInList;
  122. }
  123. /************************************************************************************************************************/
  124. /// <summary>Adds the `item` to the end of this list.</summary>
  125. /// <exception cref="ArgumentException">The `item` was already in a keyed list.</exception>
  126. public void Add(T item)
  127. {
  128. var key = item.Key;
  129. // Make sure it isn't already in a list.
  130. if (key._Index != NotInList)
  131. throw new ArgumentException(SingleUse);
  132. // Set the index of the new item and add it to the list.
  133. key._Index = Items.Count;
  134. Items.Add(item);
  135. }
  136. /// <summary>Adds the `item` to the end of this list if it wasn't already in it.</summary>
  137. public void AddNew(T item)
  138. {
  139. if (!Contains(item))
  140. Add(item);
  141. }
  142. /************************************************************************************************************************/
  143. /// <summary>Adds the `item` to this list at the specified `index`.</summary>
  144. public void Insert(int index, T item)
  145. {
  146. for (int i = index; i < Items.Count; i++)
  147. Items[i].Key._Index++;
  148. item.Key._Index = index;
  149. Items.Insert(index, item);
  150. }
  151. /************************************************************************************************************************/
  152. /// <summary>Removes the item at the specified `index`.</summary>
  153. public void RemoveAt(int index)
  154. {
  155. // Adjust the indices of all items after the target.
  156. for (int i = index + 1; i < Items.Count; i++)
  157. Items[i].Key._Index--;
  158. // Mark the key as removed and remove the item.
  159. Items[index].Key._Index = NotInList;
  160. Items.RemoveAt(index);
  161. }
  162. /// <summary>Removes the item at the specified `index` by swapping the last item in this list into its place.</summary>
  163. /// <remarks>
  164. /// This does not maintain the order of items, but is more efficient than <see cref="RemoveAt"/> because
  165. /// it avoids the need to move every item after the target down one place.
  166. /// </remarks>
  167. public void RemoveAtSwap(int index)
  168. {
  169. // Mark the item as removed.
  170. Items[index].Key._Index = NotInList;
  171. // If it wasn't the last item, move the last item over it.
  172. var lastIndex = Items.Count - 1;
  173. if (lastIndex > index)
  174. {
  175. var lastItem = Items[lastIndex];
  176. lastItem.Key._Index = index;
  177. Items[index] = lastItem;
  178. }
  179. // Remove the last item from the list.
  180. Items.RemoveAt(lastIndex);
  181. }
  182. /************************************************************************************************************************/
  183. /// <summary>Removes the `item` from this list.</summary>
  184. /// <exception cref="ArgumentException">The `item` is not in this list.</exception>
  185. public bool Remove(T item)
  186. {
  187. var key = item.Key;
  188. var index = key._Index;
  189. // If it isn't in a list, do nothing.
  190. if (index == NotInList)
  191. return false;
  192. // Make sure the item is actually in this list at the index it says.
  193. // Otherwise it must be in a different list.
  194. if (Items[index] != item)
  195. throw new ArgumentException(NotFound, nameof(item));
  196. // Remove the item.
  197. RemoveAt(index);
  198. return true;
  199. }
  200. /************************************************************************************************************************/
  201. /// <summary>Removes the `item` by swapping the last item in this list into its place.</summary>
  202. /// <remarks>
  203. /// This does not maintain the order of items, but is more efficient than <see cref="Remove"/> because
  204. /// it avoids the need to move every item after the target down one place.
  205. /// </remarks>
  206. /// <exception cref="ArgumentException">The `item` is not in this list.</exception>
  207. public bool RemoveSwap(T item)
  208. {
  209. var key = item.Key;
  210. var index = key._Index;
  211. // If it isn't in a list, do nothing.
  212. if (index == NotInList)
  213. return false;
  214. // Make sure the item is actually in this list at the index it says.
  215. // Otherwise it must be in a different list.
  216. if (Items[index] != item)
  217. throw new ArgumentException(NotFound, nameof(item));
  218. // Remove the item.
  219. RemoveAtSwap(index);
  220. return true;
  221. }
  222. /************************************************************************************************************************/
  223. /// <summary>Removes all items from this list.</summary>
  224. public void Clear()
  225. {
  226. for (int i = Items.Count - 1; i >= 0; i--)
  227. Items[i].Key._Index = NotInList;
  228. Items.Clear();
  229. }
  230. /************************************************************************************************************************/
  231. /// <summary>Copies all the items from this list into the `array`, starting at the specified `index`.</summary>
  232. public void CopyTo(T[] array, int index) => Items.CopyTo(array, index);
  233. /// <summary>Copies all the items from this list into the `array`, starting at the specified `index`.</summary>
  234. void ICollection.CopyTo(Array array, int index) => ((ICollection)Items).CopyTo(array, index);
  235. /// <summary>Returns false.</summary>
  236. bool ICollection<T>.IsReadOnly => false;
  237. /// <summary>Returns an enumerator that iterates through this list.</summary>
  238. public List<T>.Enumerator GetEnumerator() => Items.GetEnumerator();
  239. /// <inheritdoc/>
  240. IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
  241. /// <inheritdoc/>
  242. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  243. /************************************************************************************************************************/
  244. /// <summary>Is this list thread safe?</summary>
  245. bool ICollection.IsSynchronized => ((ICollection)Items).IsSynchronized;
  246. /// <summary>An object that can be used to synchronize access to this <see cref="ICollection"/>.</summary>
  247. object ICollection.SyncRoot => ((ICollection)Items).SyncRoot;
  248. /************************************************************************************************************************/
  249. }
  250. /************************************************************************************************************************/
  251. }
  252. }