// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections; using System.Collections.Generic; namespace Animancer { /// Stores the index of an object in a . /// https://kybernetik.com.au/animancer/api/Animancer/Key /// public class Key : Key.IListItem { /************************************************************************************************************************/ /// An object with a so it can be used in a . /// /// It's usually easiest to just inherit from , but otherwise the recommended /// implementation looks like this: /// /// class MyClass : Key.IListItem /// { /// Key Key.IListItem.Key { get; } = new Key(); /// // Don't use expression bodied ...Key => new... because that would create a new one every time. /// } /// /// https://kybernetik.com.au/animancer/api/Animancer/IListItem /// public interface IListItem { /// /// The which stores the index of this object. /// Key Key { get; } } /************************************************************************************************************************/ /// The which indicates that an item isn't in a list. public const int NotInList = -1; /// The current position of this key in the list. private int _Index = -1; /// Returns location of this object in the list (or -1 if it is not currently in a keyed list). public static int IndexOf(Key key) => key._Index; /// Is the `key` currently in a keyed list? public static bool IsInList(Key key) => key._Index != NotInList; /************************************************************************************************************************/ /// A is its own . Key IListItem.Key => this; /************************************************************************************************************************/ /// A which can remove items without needing to search the entire collection. /// /// This implementation has several restrictions compared to a regular : /// /// Items must implement or inherit from . /// Items cannot be null. /// Items can only be in one at a time and cannot appear multiple times in it. /// /// This class is nested inside so it can modify the private without /// exposing that capability to anything else. /// /// https://kybernetik.com.au/animancer/api/Animancer/KeyedList_1 /// public class KeyedList : IList, ICollection where T : class, IListItem { /************************************************************************************************************************/ private const string SingleUse = "Each item can only be used in one " + nameof(KeyedList) + " at a time.", NotFound = "The specified item does not exist in this " + nameof(KeyedList) + "."; /************************************************************************************************************************/ private readonly List Items; /************************************************************************************************************************/ /// Creates a new using the default constructor. public KeyedList() => Items = new List(); /// Creates a new with the specified initial `capacity`. public KeyedList(int capacity) => Items = new List(capacity); // No copy constructor because the keys will not work if they are used in multiple lists at once. /************************************************************************************************************************/ /// The number of items currently in the list. public int Count => Items.Count; /// The number of items that this list can contain before resizing is required. public int Capacity { get => Items.Capacity; set => Items.Capacity = value; } /************************************************************************************************************************/ /// The item at the specified `index`. /// The `value` was already in a keyed list (setter only). public T this[int index] { get => Items[index]; set { var key = value.Key; // Make sure it isn't already in a list. if (key._Index != NotInList) throw new ArgumentException(SingleUse); // Remove the old item at that index. Items[index].Key._Index = NotInList; // Set the index of the new item and add it at that index. key._Index = index; Items[index] = value; } } /************************************************************************************************************************/ /// Indicates whether the `item` is currently in this list. public bool Contains(T item) { if (item == null) return false; var index = item.Key._Index; return (uint)index < (uint)Items.Count && Items[index] == item; } /************************************************************************************************************************/ /// Returns the index of the `item` in this list or -1 if it is not in this list. public int IndexOf(T item) { if (item == null) return NotInList; var index = item.Key._Index; if ((uint)index < (uint)Items.Count && Items[index] == item) return index; else return NotInList; } /************************************************************************************************************************/ /// Adds the `item` to the end of this list. /// The `item` was already in a keyed list. public void Add(T item) { var key = item.Key; // Make sure it isn't already in a list. if (key._Index != NotInList) throw new ArgumentException(SingleUse); // Set the index of the new item and add it to the list. key._Index = Items.Count; Items.Add(item); } /// Adds the `item` to the end of this list if it wasn't already in it. public void AddNew(T item) { if (!Contains(item)) Add(item); } /************************************************************************************************************************/ /// Adds the `item` to this list at the specified `index`. public void Insert(int index, T item) { for (int i = index; i < Items.Count; i++) Items[i].Key._Index++; item.Key._Index = index; Items.Insert(index, item); } /************************************************************************************************************************/ /// Removes the item at the specified `index`. public void RemoveAt(int index) { // Adjust the indices of all items after the target. for (int i = index + 1; i < Items.Count; i++) Items[i].Key._Index--; // Mark the key as removed and remove the item. Items[index].Key._Index = NotInList; Items.RemoveAt(index); } /// Removes the item at the specified `index` by swapping the last item in this list into its place. /// /// This does not maintain the order of items, but is more efficient than because /// it avoids the need to move every item after the target down one place. /// public void RemoveAtSwap(int index) { // Mark the item as removed. Items[index].Key._Index = NotInList; // If it wasn't the last item, move the last item over it. var lastIndex = Items.Count - 1; if (lastIndex > index) { var lastItem = Items[lastIndex]; lastItem.Key._Index = index; Items[index] = lastItem; } // Remove the last item from the list. Items.RemoveAt(lastIndex); } /************************************************************************************************************************/ /// Removes the `item` from this list. /// The `item` is not in this list. public bool Remove(T item) { var key = item.Key; var index = key._Index; // If it isn't in a list, do nothing. if (index == NotInList) return false; // Make sure the item is actually in this list at the index it says. // Otherwise it must be in a different list. if (Items[index] != item) throw new ArgumentException(NotFound, nameof(item)); // Remove the item. RemoveAt(index); return true; } /************************************************************************************************************************/ /// Removes the `item` by swapping the last item in this list into its place. /// /// This does not maintain the order of items, but is more efficient than because /// it avoids the need to move every item after the target down one place. /// /// The `item` is not in this list. public bool RemoveSwap(T item) { var key = item.Key; var index = key._Index; // If it isn't in a list, do nothing. if (index == NotInList) return false; // Make sure the item is actually in this list at the index it says. // Otherwise it must be in a different list. if (Items[index] != item) throw new ArgumentException(NotFound, nameof(item)); // Remove the item. RemoveAtSwap(index); return true; } /************************************************************************************************************************/ /// Removes all items from this list. public void Clear() { for (int i = Items.Count - 1; i >= 0; i--) Items[i].Key._Index = NotInList; Items.Clear(); } /************************************************************************************************************************/ /// Copies all the items from this list into the `array`, starting at the specified `index`. public void CopyTo(T[] array, int index) => Items.CopyTo(array, index); /// Copies all the items from this list into the `array`, starting at the specified `index`. void ICollection.CopyTo(Array array, int index) => ((ICollection)Items).CopyTo(array, index); /// Returns false. bool ICollection.IsReadOnly => false; /// Returns an enumerator that iterates through this list. public List.Enumerator GetEnumerator() => Items.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /************************************************************************************************************************/ /// Is this list thread safe? bool ICollection.IsSynchronized => ((ICollection)Items).IsSynchronized; /// An object that can be used to synchronize access to this . object ICollection.SyncRoot => ((ICollection)Items).SyncRoot; /************************************************************************************************************************/ } /************************************************************************************************************************/ } }