Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does enumerating through a collection throw an exception but looping through its items does not

I was testing out some synchronization constructs and I noticed something that confused me. When I was enumerating through a collection while writing to it at the same time, it threw an exception (this was expected), but when I looped through the collection using a for loop, it did not. Can someone explain this? I thought that a List does not allow a reader and writer to operate at the same time. I would have expected looping through the collection to exhibit the same behavior as using an enumerator.

UPDATE: This is a purely academic exercise. I undersand that enumerating a list is bad if it is being written to at the same time. I also understand that I need a synchronization construct. My question again was about why operation one throws an exception as expected but the other does not.

Code is below:

   class Program
   {
    private static List<string> _collection = new List<string>();
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddItems), null);
        System.Threading.Thread.Sleep(5000);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DisplayItems), null);
        Console.ReadLine();
    }

    public static void AddItems(object state_)
    {
        for (int i = 1; i <= 50; i++)
        {
            _collection.Add(i.ToString());
            Console.WriteLine("Adding " + i);
            System.Threading.Thread.Sleep(150);
        }
    }

    public static void DisplayItems(object state_)
    {
        // This will not throw an exception
        //for (int i = 0; i < _collection.Count; i++)
        //{
        //    Console.WriteLine("Reading " + _collection[i]);
        //    System.Threading.Thread.Sleep(150);
        //}

        // This will throw an exception
        List<string>.Enumerator enumerator = _collection.GetEnumerator();
        while (enumerator.MoveNext())
        {
            string value = enumerator.Current;
            System.Threading.Thread.Sleep(150);
            Console.WriteLine("Reading " + value);
        }
    }
}
like image 434
adeel825 Avatar asked Dec 02 '22 07:12

adeel825


2 Answers

You can't modify a collection while enumerating over it. That rule exists even without threading issues considered. From MSDN:

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

A integer-based for loop is not actually an enumerator. In most scenarios is accomplishes the same thing. However, the interface for IEnumerator guarantees that you can iterate through the entire collection. The platform enforces this internally by throwing an exception if a call to MoveNext occurs after the collection has been modified. This exception is thrown by the enumerator object.

The integer-based for loop only goes through its list of numbers. When you index the collection by integer, you are just getting the item at that position. If something has been inserted or deleted from the list, you may skip an item or run the same item twice. This can be useful in certain situations when you need to modify a collection while traversing it. The for loop has no enumerator object to guarantee the IEnumerator contract, therefore no exception is thrown.

like image 158
Matt Brunell Avatar answered Dec 04 '22 07:12

Matt Brunell


To answer your actual question...

When enumerating, you will get an IEnumerator that is bound to the state of the list as it was when you asked for it. Further operations operate on the enumerator(MoveNext, Current).

When using a for loop, you are making a sequence if calls to get a particular item by index. There isn't an outside context such as the enumerator that knows that you're in a loop. For all the collection knows, you're only asking for one item. Since the collection never handed out an enumerator, there's no way for it to know that the reason you're asking for item 0, then item 1, then item 2, etc is because you're walking the list.

If you're mucking with the list at the same time as walking it, you're going to get errors either way. If adding items, then the for loop may skip some silently, while the foreach loop will throw. If removing items, then the for loop may throw an index out of range if you're unlucky, but will probably work most of the time.

But I think you understand all that, your question was simply why two ways of iterating behaved differently. The answer to that is the state of the collection is known(to the collection) when you call GetEnumerator in one case, and when you call get_Item in the other case.

like image 43
Darren Clark Avatar answered Dec 04 '22 09:12

Darren Clark