Sunday, October 14, 2012

Loop-safe List

I recently worked on a problem (of while I will probably write about later in more detail) that required looping over the elements in a list, and ran into this classic mistake:

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> toRemove = new List<int>() { 2, 4, 6, 8 };
foreach(int item in list)
{
    if(toRemove.Contains(item))
    {
        list.Remove(item);
    }
}


Of course when this code runs, you immediately get the "Collection was modified; enumeration may not execute." error. Easily fixed by looping over a copy of the original list:

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> toRemove = new List<int>() { 2, 4, 6, 8 };
List<int> listCopy = new List<int>(list);
foreach(int item in listCopy)
{
    if(toRemove.Contains(item))
    {
        list.Remove(item);
    }
}


So, I created my own "loop-safe" list class, called LoopList, as so:

/// <summary>
/// List that allows modification inside a foreach loop.
/// </summary>
/// <typeparam name="T">The type of object contained in the list.</typeparam>
public class LoopList<T> : IList<T>
{
    /// <summary>
    /// The items in the LoopList.
    /// </summary>
    List<T> _items;

    /// <summary>
    /// Default LoopList constructor, takes an optional isReadOnly flag.
    /// </summary>
    /// <param name="isReadOnly">Whether this will be a read-only collection.</param>
    public LoopList(bool isReadOnly = false)
    {
        _items = new List<T>();
        IsReadOnly = isReadOnly;
    }

    /// <summary>
    /// Constructor that takes an input collection to copy, also takes an optional isReadOnly flag.
    /// </summary>
    /// <param name="collection">The input collection to copy.</param>
    /// <param name="isReadOnly">Whether this will be a read-only collection.</param>
    public LoopList(IEnumerable<T> collection, bool isReadOnly = false)
    {
        _items = new List<T>(collection);
        IsReadOnly = isReadOnly;
    }

    /// <summary>
    /// Constructor that takes an initial capacity to allocate, also takes an optional isReadOnly flag.
    /// </summary>
    /// <param name="capacity">The initial capacity to allocate.</param>
    /// <param name="isReadOnly">Whether this will be a read-only collection.</param>
    public LoopList(int capacity, bool isReadOnly = false)
    {
        _items = new List<T>(capacity);
        IsReadOnly = isReadOnly;
    }

    /// <summary>
    /// Get the Index of an item in the LoopList.
    /// </summary>
    /// <param name="item">The item to find the index of.</param>
    /// <<returns>The index of the specified item.</returns>
    public int IndexOf(T item)
    {
        return _items.IndexOf(item);
    }

    /// <summary>
    /// Insert an item at the specified index.
    /// </summary>
    /// <param name="index">The index at which to insert the item.</param>
    /// <param name="item">The item to insert.</param>
    public void Insert(int index, T item)
    {
        if (IsReadOnly)
        {
            throw new InvalidOperationException("Attempt to Insert item into ReadOnly LoopList");
        }
        _items.Insert(index, item);
    }

    /// <summary>
    /// Remove the item at the specified index.
    /// </summary>
    /// <param name="index">The index of the item to remove.</param>
    public void RemoveAt(int index)
    {
        if (IsReadOnly)
        {
            throw new InvalidOperationException("Attempt to Remove item from ReadOnly LoopList");
        }
        _items.RemoveAt(index);
    }

    /// <summary>
    /// Get or set the item at a specified index.
    /// </summary>
    /// <param name="index">The index of the item to get or set.</param>
    /// <returns>The item at the specified index.</returns>
    public T this[int index]
    {
        get
        {
            return _items[index];
        }
        set
        {
            if (IsReadOnly)
            {
                throw new InvalidOperationException("Attempt to set item value in ReadOnly LoopList");
            }
            _items[index] = value;
        }
    }

    /// <summary>
    /// Add an item to the LoopList.
    /// </summary>
    /// <param name="item">The item to add.</param>
    public void Add(T item)
    {
        if (IsReadOnly)
        {
            throw new InvalidOperationException("Attempt to Add item to ReadOnly LoopList");
        }
        _items.Add(item);
    }

    /// <summary>
    /// Remove all items from the LoopList.
    /// </summary>
    public void Clear()
    {
        if (IsReadOnly)
        {
            throw new InvalidOperationException("Attempt to Clear ReadOnly LoopList");
        }
        _items.Clear();
    }

    /// <summary>
    /// Check if the LoopList contains an item.
    /// </summary>
    /// <param name="item">The item to look for.</param>
    /// <<returns>True if the LoopList contains the item, false otherwise.</returns>
    public bool Contains(T item)
    {
        return _items.Contains(item);
    }

    /// <summary>
    /// Copy the items in the LoopList to an array.
    /// </summary>
    /// <param name="array">The array to copy items into.</param>
    /// <param name="arrayIndex">The index in the array to start copying items to.</param>
    public void CopyTo(T[] array, int arrayIndex)
    {
        _items.CopyTo(array, arrayIndex);
    }
        
    /// <summary>
    /// The number of items in the LoopList.
    /// </summary>
    public int Count
    {
        get { return _items.Count; }
    }

    /// <summary>
    /// A flag indicating whether the LoopList is read-only.
    /// </summary>
    public bool IsReadOnly
    {
        get;
        private set;
    }

    /// <summary>
    /// Remove the specified item from the LoopList.
    /// </summary>
    /// <param name="item">The item to remove.</param>
    /// <returns>True if the item was successfully removed, false otherwise.</returns>
    public bool Remove(T item)
    {
        if (IsReadOnly)
        {
            throw new InvalidOperationException("Attempt to Remove item from ReadOnly LoopList");
        }
        return _items.Remove(item);
    }

    /// <summary>
    /// Get an enumerator to loop over the items in the LoopList.
    /// </summary>
    /// <returns>An Enumerator over items of the type contained in the LoopList.</returns>
    public IEnumerator<T> GetEnumerator()
    {
        List<T> tempItems = new List<T>(_items);
        return tempItems.GetEnumerator();
    }

    /// <summary>
    /// Get an enumerator to loop over the items in the LoopList.
    /// </summary>
    /// <returns>An Enumerator over items of the type contained in the LoopList.</returns>
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}


And, voila! It works:

LoopList<int> list = new LoopList<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> toRemove = new List<int>() { 2, 4, 6, 8 };
foreach (int item in list)
{
    if (toRemove.Contains(item))
    {
        list.Remove(item);
    }
}

No comments: