tags:

views:

2094

answers:

10

I and trying to build a pie chart from a dictionary. Before I display the pie chart, I want to tidy up the data. I'm removing any pie slices that would be less than 5% of the pie and putting them in a "Other" pie slice. However I'm getting a Collection was modified; enumeration operation may not execute exception at runtime.

I understand why you can not add or remove items from a dictionary while iterating over them. However I don't understand why you can't simply change a value for an existing key within the foreach loop.

Any suggestions re: fixing my code, would be appreciated.

Dictionary<string, int> colStates = new Dictionary<string,int>();
// ...
// Some code to populate colStates dictionary
// ...

int OtherCount = 0;

foreach(string key in colStates.Keys)
{

    double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

colStates.Add("Other", OtherCount);
+12  A: 

Setting a value in a dictionary updates its internal "version number" - which invalidates the iterator, and any iterator associated with the keys or values collection.

I do see your point, but at the same time it would be odd if the values collection could change mid-iteration - and for simplicity there's only one version number.

The normal way of fixing this sort of thing is to either copy the collection of keys beforehand and iterate over the copy, or iterate over the original collection but maintain a collection of changes which you'll apply after you've finished iterating.

For example:

Copying keys first

List<string> keys = new List<string>(colStates.Keys);
foreach(string key in keys)
{

    double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

Or...

Creating a list of modifications

List<string> keysToNuke = new List<string>();
foreach(string key in colStates.Keys)
{

    double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        keysToNuke.Add(key);
    }
}
foreach (string key in keysToNuke)
{
    colStates[key] = 0;
}
Jon Skeet
+1  A: 

You need to create a new Dictionary from the old rather than modifying in place. Somethine like (also iterate over the KeyValuePair<,> rather than using a key lookup:

int otherCount = 0;
int totalCounts = colStates.Values.Sum();
var newDict = new Dictionary<string,int>();
foreach (var kv in colStates) {
  if (kv.Value/(double)totalCounts < 0.05) {
    otherCount += kv.Value;
  } else {
    newDict.Add(kv.Key, kv.Value);
  }
}
if (otherCount > 0) {
  newDict.Add("Other", otherCount);
}

colStates = newDict;
Richard
A: 

Disclaimer: I don't do much C#

You are trying to modify the DictionaryEntry object which is stored in the HashTable. The Hashtable only stores one object -- your instance of DictionaryEntry. Changing the Key or the Value is enough to change the HashTable and cause the enumerator to become invalid.

You can do it outside of the loop:

if(hashtable.Contains(key))
{
    hashtable[key] = value;
}

by first creating a list of all the keys of the values you wish to change and iterate through that list instead.

Cambium
A: 

You can't modify the collection, not even the values. You could save these cases and remove them later. It would end up like this:

        Dictionary<string, int> colStates = new Dictionary<string, int>();
        // ...
        // Some code to populate colStates dictionary
        // ...

        int OtherCount = 0;
        List<string> notRelevantKeys = new List<string>();

        foreach (string key in colStates.Keys)
        {

            double Percent = colStates[key] / colStates.Count;

            if (Percent < 0.05)
            {
                OtherCount += colStates[key];
                notRelevantKeys.Add(key);
            }
        }

        foreach (string key in notRelevantKeys)
        {
            colStates[key] = 0;
        }

        colStates.Add("Other", OtherCount);
Samuel Carrijo
A: 

You are modifying the collection in this line:

colStates[key] = 0;

By doing so, you are essentially deleting and reinserting something at that point (as far as IEnumerable is concerned anyways.

If you edit a member of the value you are storing, that would be OK, but you are editing the value itself and IEnumberable doesn't like that.

The solution I've used is to eliminate the foreach loop and just use a for loop. A simple for loop won't check for changes that you know won't effect the collection.

Here's how you could do it:

List<string> keys = new List<string>(colStates.Keys);
for(int i = 0; i < keys.Count; i++)
{
    string key = keys[i];
    double  Percent = colStates[key] / TotalCount;
    if (Percent < 0.05)    
    {        
        OtherCount += colStates[key];
        colStates[key] = 0;    
    }
}
CodeFusionMobile
A: 

"The foreach statement is a wrapper around the enumerator, which allows only reading from the collection, not writing to it." See here.

SpaceghostAli
A: 

You can't modify the keys nor the values directly in a ForEach, but you can modify their members. E.g., this should work:

public class State {
    public int Value;
}

...

Dictionary<string, State> colStates = new Dictionary<string,State>();

int OtherCount = 0;
foreach(string key in colStates.Keys)
{
    double  Percent = colStates[key].Value / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key].Value;
        colStates[key].Value = 0;
    }
}

colStates.Add("Other", new State { Value =  OtherCount } );
Jeremy Frey
+1  A: 

How about just doing some linq queries against your dictionary, and then binding your graph to the results of those?...

var under = colStates.Where(c => (decimal)c.Value / (decimal)totalCount < .05M);
var over = colStates.Where(c => (decimal)c.Value / (decimal)totalCount >= .05M);
var newColStates = over.Union(new Dictionary<string, int>() { { "Other", under.Sum(c => c.Value) } });

foreach (var item in newColStates)
{
    Console.WriteLine("{0}:{1}", item.Key, item.Value);
}
Scott Ivey
Isn't Linq only availabe in 3.5? I'm using .net 2.0.
Aheho
You can use it from 2.0 with a reference to the 3.5 version of System.Core.DLL - if that's not something you'd want to undertake let me know and I'll delete this answer.
Scott Ivey
I probably wont go this route, but It's a good suggestion nevertheless.I suggest you leave the answer in place in case someone else with the same issue stumbles across it.
Aheho
A: 

If you're feeling creative you could do something like this. Loop backwards through the dictionary to make your changes.

Dictionary<string, int> collection = new Dictionary<string, int>();
collection.Add("value1", 9);
collection.Add("value2", 7);
collection.Add("value3", 5);
collection.Add("value4", 3);
collection.Add("value5", 1);

for (int i = collection.Keys.Count; i-- > 0; ) {
    if (collection.Values.ElementAt(i) < 5) {
        collection.Remove(collection.Keys.ElementAt(i)); ;
    }

}

Certainly not identical, but you might be interested anyways...

Hugoware
A: 

This piece of code:

IterIsolate

was originally written by Eric Gunnerson (although I can't find the original link), and isolates your iterator to avoid this error. You call it like so:

foreach (string k in new IterIsolate(list.Keys)) {}
quillbreaker