/Utilities/Collections/ChangeableList.cs
C# | 662 lines | 372 code | 54 blank | 236 comment | 27 complexity | 6a157db65e4d27f88990dd1f7976acb9 MD5 | raw file
Possible License(s): Apache-2.0
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using Delta.Utilities.Helpers;
- using NUnit.Framework;
-
- namespace Delta.Utilities.Collections
- {
- /// <summary>
- /// Changeable list is basically just a List, which allows
- /// you to add and remove elements while enumerating.
- /// When ALL enumerations are complete, all the remembered removed
- /// and added elements will be applied (we can't do that while enumerating).
- /// </summary>
- /// <remarks>
- /// You can use this list like a normal List and will never notice
- /// any difference except when adding or removing entries while enumerating,
- /// which will thrown an exception for normal Lists.
- /// When the enumeration is complete and there are no other pending
- /// enumerations, the ApplyAddedAndRemovedElements() function is called
- /// and all added and removed elements are added and removed.
- /// Another way would be to copy the whole list for the enumeration,
- /// but this is much slower and usually we don't change the list that
- /// often when enumerating (see slower approach here:)
- /// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp01212002.asp
- /// </remarks>
- /// <typeparam name="T">T</typeparam>
- public class ChangeableList<T>
- : IEnumerable<T>, ICollection<T>,
- ICloneable<ChangeableList<T>>
- {
- #region ChangeableEnumerator Class
- /// <summary>
- /// Changeable enumerator
- /// </summary>
- public class ChangeableEnumerator : IEnumerator<T>, IDisposable
- {
- #region currentIndex (Public)
- /// <summary>
- /// Own index in list.
- /// </summary>
- public int currentIndex = -1;
- #endregion
-
- #region Current (Public)
- /// <summary>
- /// Current item for enumerating.
- /// </summary>
- public T Current
- {
- get
- {
- if (currentIndex < 0 ||
- currentIndex >= list.Count)
- {
- return default(T);
- }
- else
- {
- return list[currentIndex];
- }
- } // get
- }
- #endregion
-
- #region Private
-
- #region list (Private)
- /// <summary>
- /// The list we are working on.
- /// </summary>
- private readonly ChangeableList<T> list;
- #endregion
-
- #region enumerationStarted (Private)
- /// <summary>
- /// Started enumeration?
- /// </summary>
- private bool enumerationStarted;
- #endregion
-
- #region disposed (Private)
- /// <summary>
- /// Disposed
- /// </summary>
- /// <returns>False</returns>
- private bool disposed;
- #endregion
-
- #region Current (Private)
- /// <summary>
- /// IEnumerator. current
- /// </summary>
- /// <returns>Object</returns>
- object IEnumerator.Current
- {
- get
- {
- return Current;
- } // get
- }
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Create changeable enumerator
- /// </summary>
- public ChangeableEnumerator(ChangeableList<T> setList)
- {
- list = setList;
- if (enumerationStarted == false)
- {
- list.enumerationCount++;
- }
- enumerationStarted = true;
- }
- #endregion
-
- #region Destructor
- /// <summary>
- /// Finalizer.
- /// </summary>
- ~ChangeableEnumerator()
- {
- Dispose();
- }
- #endregion
-
- #region IDisposable Members
- /// <summary>
- /// Dispose
- /// </summary>
- public void Dispose()
- {
- if (disposed)
- {
- return;
- }
- disposed = true;
- if (enumerationStarted)
- {
- list.ReduceEnumerationCount();
- }
- enumerationStarted = false;
- GC.SuppressFinalize(this);
- }
- #endregion
-
- #region IEnumerator Members
- /// <summary>
- /// Move next for enumeration.
- /// </summary>
- /// <returns>True if there was a next entry</returns>
- public bool MoveNext()
- {
- currentIndex++;
- bool ret = currentIndex < list.Count;
- if (ret == false)
- {
- if (enumerationStarted)
- {
- list.ReduceEnumerationCount();
- }
- enumerationStarted = false;
- } // if (ret)
- return ret;
- }
-
- /// <summary>
- /// Reset enumeration.
- /// </summary>
- public void Reset()
- {
- currentIndex = -1;
- if (enumerationStarted == false)
- {
- list.enumerationCount++;
- }
- enumerationStarted = true;
- }
- #endregion
- }
- #endregion
-
- #region Item (Public)
- /// <summary>
- /// This
- /// </summary>
- /// <param name="index">Index</param>
- /// <returns>T</returns>
- public T this[int index]
- {
- get
- {
- return data[index];
- } // get
- set
- {
- data[index] = value;
- } // set
- }
- #endregion
-
- #region Count (Public)
- /// <summary>
- /// Count
- /// </summary>
- /// <typeparam name="T">T</typeparam>
- /// <returns>Int</returns>
- public int Count
- {
- get
- {
- return data.Count;
- } // get
- }
- #endregion
-
- #region IsReadOnly (Public)
- /// <summary>
- /// Is read only? Will always return false.
- /// </summary>
- public bool IsReadOnly
- {
- get
- {
- return false;
- } // get
- }
- #endregion
-
- #region Protected
-
- #region remElementsToBeAdded (Protected)
- /// <summary>
- /// Remembered elements to be added and removed when finished
- /// enumerating. If we are not enumerating, adding and removing it
- /// done directly (this lists are empty then).
- /// Usually all these lists are empty, it will not happen very
- /// often that we want to add or remove elements while enumerating.
- /// </summary>
- /// <typeparam name="T">T</typeparam>
- protected List<T>
- //Remember elements to be removed
- remElementsToBeAdded = new List<T>();
- #endregion
-
- #region remElementsToBeRemoved (Protected)
- /// <summary>
- /// Remembered elements to be added and removed when finished
- /// enumerating. If we are not enumerating, adding and removing it
- /// done directly (this lists are empty then).
- /// Usually all these lists are empty, it will not happen very
- /// often that we want to add or remove elements while enumerating.
- /// </summary>
- /// <typeparam name="T">T</typeparam>
- protected List<T>
- //Remember elements to be removed
- remElementsToBeRemoved = new List<T>();
- #endregion
-
- ///// <summary>
- ///// remElementsToBeInserted is not just a list of objects, it
- ///// will use the InsertObject helper struct!
- ///// </summary>
- //protected List<InsertObject>
- // remElementsToBeInserted = new List<InsertObject>();
-
- #region enumerationCount (Protected)
- /// <summary>
- /// This value will be greater than 0 if currently in any enumeration!
- /// If this is 0, the list is modified directly, else we have to wait
- /// until all enumerations are complete.
- /// </summary>
- /// <typeparam name="T">T</typeparam>
- protected int enumerationCount;
- #endregion
-
- #endregion
-
- #region Private
-
- #region data (Private)
- /// <summary>
- /// We have to use this internal list because we can't derive from
- /// generic lists, most methods will pass on the method call to this class.
- /// </summary>
- /// <typeparam name="T">Type for the data list</typeparam>
- private readonly List<T> data = new List<T>();
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Create changeable list
- /// </summary>
- public ChangeableList()
- {
- }
-
- /// <summary>
- /// Create changeable list
- /// </summary>
- /// <param name="copyFrom">Copy data from this enumerable collection,
- /// this collection should not have any pending enumerations open, else
- /// we might miss added/removed entries.
- /// </param>
- public ChangeableList(IEnumerable<T> copyFrom)
- {
- data.InsertRange(0, copyFrom);
- }
- #endregion
-
- #region ICloneable<ChangeableList<T>> Members
- /// <summary>
- /// Clone changeable list.
- /// </summary>
- /// <returns>Cloned ChangeableList</returns>
- public ChangeableList<T> Clone()
- {
- return new ChangeableList<T>(this);
- }
- #endregion
-
- #region ICollection<T> Members
- /// <summary>
- /// Add new element to list. If we are currently enumerating, we can't
- /// add new elements, so we will remember them when all enumerations
- /// are complete. If not enumerating just add the new element.
- /// </summary>
- /// <param name="item">Item</param>
- public virtual void Add(T item)
- {
- // If currently enumerating, remember this value to add later
- if (enumerationCount > 0)
- {
- remElementsToBeAdded.Add(item);
- }
- else
- {
- data.Add(item);
- }
- }
-
- /// <summary>
- /// Clear whole list, this can actually be called inside an enumeration,
- /// the enumeration will complete and after that the whole list will
- /// be cleared!
- /// </summary>
- public void Clear()
- {
- if (enumerationCount > 0)
- {
- remElementsToBeRemoved.AddRange(data);
- }
- else
- {
- data.Clear();
- }
- }
-
- /// <summary>
- /// Does this list contain the given item?
- /// </summary>
- /// <param name="item">Item to check for</param>
- /// <returns>True if the item was found, false otherwise</returns>
- public bool Contains(T item)
- {
- return data.Contains(item);
- }
-
- /// <summary>
- /// Copy all values to target array, which must be big enough.
- /// </summary>
- /// <param name="array">Array to copy into</param>
- /// <param name="arrayIndex">Index to start copying into</param>
- public void CopyTo(T[] array, int arrayIndex)
- {
- data.CopyTo(array, arrayIndex);
- }
-
- /// <summary>
- /// Remove element from list. If we are currently enumerating, we can't
- /// remove directly, so we will remember elements to remove when all
- /// enumerations are complete. If not enumerating just remove the element.
- /// </summary>
- /// <param name="item">Item</param>
- /// <returns>
- /// True if removing the item succeeded, false otherwise.
- /// </returns>
- public bool Remove(T item)
- {
- if (enumerationCount > 0)
- {
- // Check if object does exists in list
- int index = IndexOf(item);
- if (index >= 0)
- {
- remElementsToBeRemoved.Add(item);
- }
- return index >= 0;
- } // if (enumerationCount)
-
- return data.Remove(item);
- }
- #endregion
-
- #region IEnumerable Members
- /// <summary>
- /// IEnumerable. get enumerator
- /// </summary>
- /// <returns>IEnumerator</returns>
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
- #endregion
-
- #region IEnumerable<T> Members
- /// <summary>
- /// GetEnumerator
- /// </summary>
- /// <returns>IEnumerator</returns>
- public IEnumerator<T> GetEnumerator()
- {
- return new ChangeableEnumerator(this);
- }
- #endregion
-
- #region IndexOf (Public)
- /// <summary>
- /// Index of
- /// </summary>
- /// <param name="item">Item</param>
- /// <returns>Int</returns>
- public int IndexOf(T item)
- {
- return data.IndexOf(item);
- }
- #endregion
-
- #region RemoveAt (Public)
- /// <summary>
- /// Remove element from list. If we are currently enumerating, we can't
- /// remove directly, so we will remember elements to remove when all
- /// enumerations are complete. If not enumerating just remove the element.
- /// </summary>
- /// <param name="index">Index</param>
- public void RemoveAt(int index)
- {
- if (enumerationCount > 0)
- {
- // Get index of item we want to remove
- if (index >= 0 &&
- index < data.Count)
- {
- remElementsToBeRemoved.Add(data[index]);
- }
- } // if (enumerationCount)
- else
- {
- data.RemoveAt(index);
- }
- }
- #endregion
-
- #region AddRange (Public)
- /// <summary>
- /// Add range, will just use Add to add all elements.
- /// </summary>
- /// <param name="c">C</param>
- public virtual void AddRange(ICollection<T> c)
- {
- foreach (T obj in c)
- {
- Add(obj);
- }
- }
- #endregion
-
- #region ToArray (Public)
- /// <summary>
- /// Convert this changeable list to an array with the same data.
- /// </summary>
- /// <returns>Flat fixed array created from the list</returns>
- public T[] ToArray()
- {
- return data.ToArray();
- }
- #endregion
-
- #region Methods (Private)
-
- #region ReduceEnumerationCount
- /// <summary>
- /// Reduces the enumerationCount and when reaching 0 again
- /// all the remembered elements to be added and removed will
- /// be added and removed. This function is called from the enumerator.
- /// </summary>
- protected void ReduceEnumerationCount()
- {
- if (enumerationCount <= 0)
- {
- throw new InvalidOperationException(
- "Can't call ReduceEnumerationCount when enumerationCount is 0!");
- }
-
- enumerationCount--;
- // When reaching 0, execute all remembered elements
- if (enumerationCount == 0)
- {
- if (remElementsToBeAdded.Count == 0 &&
- //remElementsToBeInserted.Count == 0 &&
- remElementsToBeRemoved.Count == 0)
- {
- // In most cases nothing was added, just quit then
- return;
- }
-
- //foreach (InsertObject insertObj in remElementsToBeInserted)
- //{
- // data.Insert(insertObj.index, insertObj.obj);
- //}
- //remElementsToBeInserted.Clear();
-
- foreach (T obj in remElementsToBeAdded)
- {
- data.Add(obj);
- }
- remElementsToBeAdded.Clear();
-
- // Rem elements to be removed
- foreach (T obj in remElementsToBeRemoved)
- {
- data.Remove(obj);
- }
- remElementsToBeRemoved.Clear();
- } // if
- }
- #endregion
-
- #endregion
- }
-
- /// <summary>
- /// ChangeableList tests, must be declared outside of a generic class.
- /// </summary>
- internal class ChangeableListTests
- {
- #region TestChangeableList
- /// <summary>
- /// Test ChangeableList. Note: Too slow for a dynamic unit test.
- /// </summary>
- [Test]
- public void TestChangeableList()
- {
- // Create list
- ChangeableList<int> list = new ChangeableList<int>();
- list.Add(1);
- list.Add(3);
- list.Add(5);
-
- // Add element while enumerating (7) and remove all elements
- // with a value above 1 (except 5, which will add 7).
- // This will not work with other lists, only Changeable lists allow this!
- foreach (int num in list)
- {
- //Console.WriteLine("Num: " + num);
- if (num == 5)
- {
- list.Add(7);
- }
- else if (num > 1)
- {
- list.Remove(num);
- }
- } // foreach
- // The resulting list should now have changed from "1, 3, 5" to "1, 5, 7"
- Assert.Equal("1, 5, 7", list.Write());
-
- //Console.WriteLine("Before insert: "+ArrayHelper.Write(list));
- // Insert range of elements while enumerating
- //foreach (int num in list)
- //{
- //if (num == 5)
- // // Just add a couple of 2s at position 2
- // list.InsertRange(2, new int[] { 2, 2, 2 });
- // if (num == 7)
- // // Enumerate again, just for the fun of it
- // foreach (int num2 in list)
- // {
- // Assert.False(num2 == 2);
- // if (num2 == 1)
- // list.Insert(3, 10);
- // list.Remove(10);
- // } // if foreach
- //} // foreach
-
- // Ok, check if list contains "1, 5, 2, 10, 2, 2, 7" as expected
- //Assert.Equal("1, 5, 2, 10, 2, 2, 7",
- // ArrayHelper.Write(list));
- //Console.WriteLine("After insert: " + ArrayHelper.Write(list));
-
- // Do same stuff with a normal ArrayList and compare!
- List<int> cmpList = new List<int>();
- cmpList.AddRange(new[]
- {
- 1, 5
- });
- cmpList.InsertRange(2, new[]
- {
- 7, 7
- });
- cmpList.Remove(7);
- cmpList.Insert(3, 10);
- cmpList.RemoveAt(3);
- //Console.WriteLine("cmpList: " + ArrayHelper.Write(cmpList));
- Assert.True(ArrayHelper.Compare(
- list.ToArray(),
- cmpList.ToArray()));
- }
- #endregion
-
- #region TestCloningChangeableList
- /// <summary>
- /// Test cloning changeable list
- /// </summary>
- [Test]
- public void TestCloningChangeableList()
- {
- ChangeableList<int> testList = new ChangeableList<int>();
- testList.Add(1);
- testList.Add(2);
- testList.Add(3);
-
- foreach (int num1 in testList)
- {
- Assert.Equal(1, num1);
- // This will be added after the foreach loop!
- testList.Add(1);
- ChangeableList<int> testList2 = testList.Clone();
- foreach (int num2 in testList2)
- {
- Assert.Equal(1, num2);
- // This will be added after the internal foreach loop.
- testList2.Add(2);
- // The lists should be different
- Assert.False(testList == testList2);
- // But the data in it should be still equal.
- Assert.Equal(testList.Write(),
- testList2.Write());
- break;
- } // foreach (num2)
- break;
- } // foreach (num1)
- }
- #endregion
- }
- }