PageRenderTime 147ms CodeModel.GetById 70ms app.highlight 17ms RepoModel.GetById 53ms app.codeStats 1ms

/Utilities/Collections/ChangeableList.cs

#
C# | 662 lines | 372 code | 54 blank | 236 comment | 27 complexity | 6a157db65e4d27f88990dd1f7976acb9 MD5 | raw file
  1using System;
  2using System.Collections;
  3using System.Collections.Generic;
  4using Delta.Utilities.Helpers;
  5using NUnit.Framework;
  6
  7namespace Delta.Utilities.Collections
  8{
  9	/// <summary>
 10	/// Changeable list is basically just a List, which allows
 11	/// you to add and remove elements while enumerating.
 12	/// When ALL enumerations are complete, all the remembered removed
 13	/// and added elements will be applied (we can't do that while enumerating).
 14	/// </summary>	
 15	/// <remarks>
 16	/// You can use this list like a normal List and will never notice
 17	/// any difference except when adding or removing entries while enumerating,
 18	/// which will thrown an exception for normal Lists.
 19	/// When the enumeration is complete and there are no other pending
 20	/// enumerations, the ApplyAddedAndRemovedElements() function is called
 21	/// and all added and removed elements are added and removed.
 22	/// Another way would be to copy the whole list for the enumeration,
 23	/// but this is much slower and usually we don't change the list that
 24	/// often when enumerating (see slower approach here:)
 25	/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp01212002.asp
 26	/// </remarks>
 27	/// <typeparam name="T">T</typeparam>
 28	public class ChangeableList<T>
 29		: IEnumerable<T>, ICollection<T>,
 30		  ICloneable<ChangeableList<T>>
 31	{
 32		#region ChangeableEnumerator Class
 33		/// <summary>
 34		/// Changeable enumerator
 35		/// </summary>
 36		public class ChangeableEnumerator : IEnumerator<T>, IDisposable
 37		{
 38			#region currentIndex (Public)
 39			/// <summary>
 40			/// Own index in list.
 41			/// </summary>
 42			public int currentIndex = -1;
 43			#endregion
 44
 45			#region Current (Public)
 46			/// <summary>
 47			/// Current item for enumerating.
 48			/// </summary>
 49			public T Current
 50			{
 51				get
 52				{
 53					if (currentIndex < 0 ||
 54					    currentIndex >= list.Count)
 55					{
 56						return default(T);
 57					}
 58					else
 59					{
 60						return list[currentIndex];
 61					}
 62				} // get
 63			}
 64			#endregion
 65
 66			#region Private
 67
 68			#region list (Private)
 69			/// <summary>
 70			/// The list we are working on.
 71			/// </summary>
 72			private readonly ChangeableList<T> list;
 73			#endregion
 74
 75			#region enumerationStarted (Private)
 76			/// <summary>
 77			/// Started enumeration?
 78			/// </summary>
 79			private bool enumerationStarted;
 80			#endregion
 81
 82			#region disposed (Private)
 83			/// <summary>
 84			/// Disposed
 85			/// </summary>
 86			/// <returns>False</returns>
 87			private bool disposed;
 88			#endregion
 89
 90			#region Current (Private)
 91			/// <summary>
 92			/// IEnumerator. current
 93			/// </summary>
 94			/// <returns>Object</returns>
 95			object IEnumerator.Current
 96			{
 97				get
 98				{
 99					return Current;
100				} // get
101			}
102			#endregion
103
104			#endregion
105
106			#region Constructors
107			/// <summary>
108			/// Create changeable enumerator
109			/// </summary>
110			public ChangeableEnumerator(ChangeableList<T> setList)
111			{
112				list = setList;
113				if (enumerationStarted == false)
114				{
115					list.enumerationCount++;
116				}
117				enumerationStarted = true;
118			}
119			#endregion
120
121			#region Destructor
122			/// <summary>
123			/// Finalizer.
124			/// </summary>
125			~ChangeableEnumerator()
126			{
127				Dispose();
128			}
129			#endregion
130
131			#region IDisposable Members
132			/// <summary>
133			/// Dispose
134			/// </summary>
135			public void Dispose()
136			{
137				if (disposed)
138				{
139					return;
140				}
141				disposed = true;
142				if (enumerationStarted)
143				{
144					list.ReduceEnumerationCount();
145				}
146				enumerationStarted = false;
147				GC.SuppressFinalize(this);
148			}
149			#endregion
150
151			#region IEnumerator Members
152			/// <summary>
153			/// Move next for enumeration.
154			/// </summary>
155			/// <returns>True if there was a next entry</returns>
156			public bool MoveNext()
157			{
158				currentIndex++;
159				bool ret = currentIndex < list.Count;
160				if (ret == false)
161				{
162					if (enumerationStarted)
163					{
164						list.ReduceEnumerationCount();
165					}
166					enumerationStarted = false;
167				} // if (ret)
168				return ret;
169			}
170
171			/// <summary>
172			/// Reset enumeration.
173			/// </summary>
174			public void Reset()
175			{
176				currentIndex = -1;
177				if (enumerationStarted == false)
178				{
179					list.enumerationCount++;
180				}
181				enumerationStarted = true;
182			}
183			#endregion
184		}
185		#endregion
186
187		#region Item (Public)
188		/// <summary>
189		/// This
190		/// </summary>
191		/// <param name="index">Index</param>
192		/// <returns>T</returns>
193		public T this[int index]
194		{
195			get
196			{
197				return data[index];
198			} // get
199			set
200			{
201				data[index] = value;
202			} // set
203		}
204		#endregion
205
206		#region Count (Public)
207		/// <summary>
208		/// Count
209		/// </summary>
210		/// <typeparam name="T">T</typeparam>
211		/// <returns>Int</returns>
212		public int Count
213		{
214			get
215			{
216				return data.Count;
217			} // get
218		}
219		#endregion
220
221		#region IsReadOnly (Public)
222		/// <summary>
223		/// Is read only? Will always return false.
224		/// </summary>
225		public bool IsReadOnly
226		{
227			get
228			{
229				return false;
230			} // get
231		}
232		#endregion
233
234		#region Protected
235
236		#region remElementsToBeAdded (Protected)
237		/// <summary>
238		/// Remembered elements to be added and removed when finished
239		/// enumerating. If we are not enumerating, adding and removing it
240		/// done directly (this lists are empty then).
241		/// Usually all these lists are empty, it will not happen very
242		/// often that we want to add or remove elements while enumerating.
243		/// </summary>
244		/// <typeparam name="T">T</typeparam>
245		protected List<T>
246			//Remember elements to be removed
247			remElementsToBeAdded = new List<T>();
248		#endregion
249
250		#region remElementsToBeRemoved (Protected)
251		/// <summary>
252		/// Remembered elements to be added and removed when finished
253		/// enumerating. If we are not enumerating, adding and removing it
254		/// done directly (this lists are empty then).
255		/// Usually all these lists are empty, it will not happen very
256		/// often that we want to add or remove elements while enumerating.
257		/// </summary>
258		/// <typeparam name="T">T</typeparam>
259		protected List<T>
260			//Remember elements to be removed
261			remElementsToBeRemoved = new List<T>();
262		#endregion
263
264		///// <summary>
265		///// remElementsToBeInserted is not just a list of objects, it
266		///// will use the InsertObject helper struct!
267		///// </summary>
268		//protected List<InsertObject>
269		//  remElementsToBeInserted = new List<InsertObject>();
270
271		#region enumerationCount (Protected)
272		/// <summary>
273		/// This value will be greater than 0 if currently in any enumeration!
274		/// If this is 0, the list is modified directly, else we have to wait
275		/// until all enumerations are complete.
276		/// </summary>
277		/// <typeparam name="T">T</typeparam>
278		protected int enumerationCount;
279		#endregion
280
281		#endregion
282
283		#region Private
284
285		#region data (Private)
286		/// <summary>
287		/// We have to use this internal list because we can't derive from
288		/// generic lists, most methods will pass on the method call to this class.
289		/// </summary>
290		/// <typeparam name="T">Type for the data list</typeparam>
291		private readonly List<T> data = new List<T>();
292		#endregion
293
294		#endregion
295
296		#region Constructors
297		/// <summary>
298		/// Create changeable list
299		/// </summary>
300		public ChangeableList()
301		{
302		}
303
304		/// <summary>
305		/// Create changeable list
306		/// </summary>
307		/// <param name="copyFrom">Copy data from this enumerable collection,
308		/// this collection should not have any pending enumerations open, else
309		/// we might miss added/removed entries.
310		/// </param>
311		public ChangeableList(IEnumerable<T> copyFrom)
312		{
313			data.InsertRange(0, copyFrom);
314		}
315		#endregion
316
317		#region ICloneable<ChangeableList<T>> Members
318		/// <summary>
319		/// Clone changeable list.
320		/// </summary>
321		/// <returns>Cloned ChangeableList</returns>
322		public ChangeableList<T> Clone()
323		{
324			return new ChangeableList<T>(this);
325		}
326		#endregion
327
328		#region ICollection<T> Members
329		/// <summary>
330		/// Add new element to list. If we are currently enumerating, we can't
331		/// add new elements, so we will remember them when all enumerations
332		/// are complete. If not enumerating just add the new element.
333		/// </summary>
334		/// <param name="item">Item</param>
335		public virtual void Add(T item)
336		{
337			// If currently enumerating, remember this value to add later
338			if (enumerationCount > 0)
339			{
340				remElementsToBeAdded.Add(item);
341			}
342			else
343			{
344				data.Add(item);
345			}
346		}
347
348		/// <summary>
349		/// Clear whole list, this can actually be called inside an enumeration,
350		/// the enumeration will complete and after that the whole list will
351		/// be cleared!
352		/// </summary>
353		public void Clear()
354		{
355			if (enumerationCount > 0)
356			{
357				remElementsToBeRemoved.AddRange(data);
358			}
359			else
360			{
361				data.Clear();
362			}
363		}
364
365		/// <summary>
366		/// Does this list contain the given item?
367		/// </summary>
368		/// <param name="item">Item to check for</param>
369		/// <returns>True if the item was found, false otherwise</returns>
370		public bool Contains(T item)
371		{
372			return data.Contains(item);
373		}
374
375		/// <summary>
376		/// Copy all values to target array, which must be big enough.
377		/// </summary>
378		/// <param name="array">Array to copy into</param>
379		/// <param name="arrayIndex">Index to start copying into</param>
380		public void CopyTo(T[] array, int arrayIndex)
381		{
382			data.CopyTo(array, arrayIndex);
383		}
384
385		/// <summary>
386		/// Remove element from list. If we are currently enumerating, we can't
387		/// remove directly, so we will remember elements to remove when all
388		/// enumerations are complete. If not enumerating just remove the element.
389		/// </summary>
390		/// <param name="item">Item</param>
391		/// <returns>
392		/// True if removing the item succeeded, false otherwise.
393		/// </returns>
394		public bool Remove(T item)
395		{
396			if (enumerationCount > 0)
397			{
398				// Check if object does exists in list
399				int index = IndexOf(item);
400				if (index >= 0)
401				{
402					remElementsToBeRemoved.Add(item);
403				}
404				return index >= 0;
405			} // if (enumerationCount)
406
407			return data.Remove(item);
408		}
409		#endregion
410
411		#region IEnumerable Members
412		/// <summary>
413		/// IEnumerable. get enumerator
414		/// </summary>
415		/// <returns>IEnumerator</returns>
416		IEnumerator IEnumerable.GetEnumerator()
417		{
418			return GetEnumerator();
419		}
420		#endregion
421
422		#region IEnumerable<T> Members
423		/// <summary>
424		/// GetEnumerator
425		/// </summary>
426		/// <returns>IEnumerator</returns>
427		public IEnumerator<T> GetEnumerator()
428		{
429			return new ChangeableEnumerator(this);
430		}
431		#endregion
432
433		#region IndexOf (Public)
434		/// <summary>
435		/// Index of
436		/// </summary>
437		/// <param name="item">Item</param>
438		/// <returns>Int</returns>
439		public int IndexOf(T item)
440		{
441			return data.IndexOf(item);
442		}
443		#endregion
444
445		#region RemoveAt (Public)
446		/// <summary>
447		/// Remove element from list. If we are currently enumerating, we can't
448		/// remove directly, so we will remember elements to remove when all
449		/// enumerations are complete. If not enumerating just remove the element.
450		/// </summary>
451		/// <param name="index">Index</param>
452		public void RemoveAt(int index)
453		{
454			if (enumerationCount > 0)
455			{
456				// Get index of item we want to remove
457				if (index >= 0 &&
458				    index < data.Count)
459				{
460					remElementsToBeRemoved.Add(data[index]);
461				}
462			} // if (enumerationCount)
463			else
464			{
465				data.RemoveAt(index);
466			}
467		}
468		#endregion
469
470		#region AddRange (Public)
471		/// <summary>
472		/// Add range, will just use Add to add all elements.
473		/// </summary>
474		/// <param name="c">C</param>
475		public virtual void AddRange(ICollection<T> c)
476		{
477			foreach (T obj in c)
478			{
479				Add(obj);
480			}
481		}
482		#endregion
483
484		#region ToArray (Public)
485		/// <summary>
486		/// Convert this changeable list to an array with the same data.
487		/// </summary>
488		/// <returns>Flat fixed array created from the list</returns>
489		public T[] ToArray()
490		{
491			return data.ToArray();
492		}
493		#endregion
494
495		#region Methods (Private)
496
497		#region ReduceEnumerationCount
498		/// <summary>
499		/// Reduces the enumerationCount and when reaching 0 again
500		/// all the remembered elements to be added and removed will
501		/// be added and removed. This function is called from the enumerator.
502		/// </summary>
503		protected void ReduceEnumerationCount()
504		{
505			if (enumerationCount <= 0)
506			{
507				throw new InvalidOperationException(
508					"Can't call ReduceEnumerationCount when enumerationCount is 0!");
509			}
510
511			enumerationCount--;
512			// When reaching 0, execute all remembered elements
513			if (enumerationCount == 0)
514			{
515				if (remElementsToBeAdded.Count == 0 &&
516				    //remElementsToBeInserted.Count == 0 &&
517				    remElementsToBeRemoved.Count == 0)
518				{
519					// In most cases nothing was added, just quit then
520					return;
521				}
522
523				//foreach (InsertObject insertObj in remElementsToBeInserted)
524				//{
525				//  data.Insert(insertObj.index, insertObj.obj);
526				//}
527				//remElementsToBeInserted.Clear();
528
529				foreach (T obj in remElementsToBeAdded)
530				{
531					data.Add(obj);
532				}
533				remElementsToBeAdded.Clear();
534
535				// Rem elements to be removed
536				foreach (T obj in remElementsToBeRemoved)
537				{
538					data.Remove(obj);
539				}
540				remElementsToBeRemoved.Clear();
541			} // if
542		}
543		#endregion
544
545		#endregion
546	}
547
548	/// <summary>
549	/// ChangeableList tests, must be declared outside of a generic class.
550	/// </summary>
551	internal class ChangeableListTests
552	{
553		#region TestChangeableList
554		/// <summary>
555		/// Test ChangeableList. Note: Too slow for a dynamic unit test.
556		/// </summary>
557		[Test]
558		public void TestChangeableList()
559		{
560			// Create list
561			ChangeableList<int> list = new ChangeableList<int>();
562			list.Add(1);
563			list.Add(3);
564			list.Add(5);
565
566			// Add element while enumerating (7) and remove all elements
567			// with a value above 1 (except 5, which will add 7).
568			// This will not work with other lists, only Changeable lists allow this!
569			foreach (int num in list)
570			{
571				//Console.WriteLine("Num: " + num);
572				if (num == 5)
573				{
574					list.Add(7);
575				}
576				else if (num > 1)
577				{
578					list.Remove(num);
579				}
580			} // foreach
581			// The resulting list should now have changed from "1, 3, 5" to "1, 5, 7"
582			Assert.Equal("1, 5, 7", list.Write());
583
584			//Console.WriteLine("Before insert: "+ArrayHelper.Write(list));
585			// Insert range of elements while enumerating
586			//foreach (int num in list)
587			//{
588			//if (num == 5)
589			//  // Just add a couple of 2s at position 2
590			//  list.InsertRange(2, new int[] { 2, 2, 2 });
591			//  if (num == 7)
592			//    // Enumerate again, just for the fun of it
593			//    foreach (int num2 in list)
594			//    {
595			//      Assert.False(num2 == 2);
596			//      if (num2 == 1)
597			//        list.Insert(3, 10);
598			//      list.Remove(10);
599			//    } // if foreach
600			//} // foreach
601
602			// Ok, check if list contains "1, 5, 2, 10, 2, 2, 7" as expected
603			//Assert.Equal("1, 5, 2, 10, 2, 2, 7",
604			//	ArrayHelper.Write(list));
605			//Console.WriteLine("After insert: " + ArrayHelper.Write(list));
606
607			// Do same stuff with a normal ArrayList and compare!
608			List<int> cmpList = new List<int>();
609			cmpList.AddRange(new[]
610			{
611				1, 5
612			});
613			cmpList.InsertRange(2, new[]
614			{
615				7, 7
616			});
617			cmpList.Remove(7);
618			cmpList.Insert(3, 10);
619			cmpList.RemoveAt(3);
620			//Console.WriteLine("cmpList: " + ArrayHelper.Write(cmpList));
621			Assert.True(ArrayHelper.Compare(
622				list.ToArray(),
623				cmpList.ToArray()));
624		}
625		#endregion
626
627		#region TestCloningChangeableList
628		/// <summary>
629		/// Test cloning changeable list
630		/// </summary>
631		[Test]
632		public void TestCloningChangeableList()
633		{
634			ChangeableList<int> testList = new ChangeableList<int>();
635			testList.Add(1);
636			testList.Add(2);
637			testList.Add(3);
638
639			foreach (int num1 in testList)
640			{
641				Assert.Equal(1, num1);
642				// This will be added after the foreach loop!
643				testList.Add(1);
644				ChangeableList<int> testList2 = testList.Clone();
645				foreach (int num2 in testList2)
646				{
647					Assert.Equal(1, num2);
648					// This will be added after the internal foreach loop.
649					testList2.Add(2);
650					// The lists should be different
651					Assert.False(testList == testList2);
652					// But the data in it should be still equal.
653					Assert.Equal(testList.Write(),
654						testList2.Write());
655					break;
656				} // foreach (num2)
657				break;
658			} // foreach (num1)
659		}
660		#endregion
661	}
662}