B
bughunter
This is partly 'for the record' and partly a query about whether the
following is a bug somewhere in .Net (whether it be the CLR, JITter, C#
compiler). This is all in the context of .Net 1.1 SP1.
Recently we (my fellow team members & I) observed an
InvalidOperationException - 'collection has been modified', however it
wasn't immediately obvious why this would be occuring. There are no
modifications occuring within the loop and there is not another thread
that could be making a change.
The object being looped over was derived from CollectionBase and the
culprit turned out to be a finalizer on the class, a finalizer that
called Clear() thus modifying the collection. My question then is, why
did the finalizer get called while the CollectionBase object was still
in scope? Or more to the point, the object has clearly gone out of
scope and so should it have done?
Here's some code to demonstrate the issue fairly reliably (on my PC)...
---------------------------------------------
// Our problematic CollectionBase class.
public class TestCollection : CollectionBase
{
public void Add(object o)
{
List.Add(o);
}
~TestCollection()
{
this.Clear();
}
}
// A helper class. This has no real relevance to the problem.
public class ListItem
{
private string itemCode;
private string itemDescription;
public ListItem(string itemCode, string itemDescription)
{
this.itemCode = itemCode;
this.itemDescription = itemDescription;
}
public override string ToString()
{
return itemDescription;
}
}
---------------------------------------------
If you then create a form in Visual Studio, drop a ComboBox and a
button on there and add the following code:
--------------------------------------------
// Creates an instance of TestCollection with some ListItems(see above)
in it.
private TestCollection BuildCol(int size)
{
TestCollection oList = new TestCollection();
for(int i=0; i<size; i++)
{
string s = i.ToString();
oList.Add(new ListItem(s,s));
}
return oList;
}
// Populates our combobox.
private void PopulateCombobox()
{
TestCollection col = BuildCol(1000);
foreach(object o in col) // exception thrown here!
{
comboBox1.Items.Add(o);
}
comboBox1.Sorted = true;
}
// From the button click event we populate the combobox several times
over to
// increase the chances of reproducing the problem.
private void button1_Click(object sender, System.EventArgs e)
{
for(int i=0; i<100; i++)
{
PopulateCombobox();
comboBox1.Items.Clear();
}
}
--------------------------------------------
Running this code generates an exception on every clicks of the button
here. If you don;t get this then you can add a call to GC.Collect()
within the foreach loop to cause a garbage collection which in turn
will invoke the TestCollection's finalizer.
What I think is happening here is that foreach is just shorthand for
something like:
--------------
IEnumerator e = col.GetEnumerator();
while(e.Current!=null)
{
// Do stuff here.
e.MoveNext();
}
--------------
So although technically 'col' is in scope for the lifetime of the loop,
in reality a code optimizer may be flagging it as out of scope since it
is not actually being used within the loop. If a garbage collection
happens to occur then the finalizer is called, modifying the collection
and bang! Of course the IEnumerator would normally have a reference to
the collection so this really doesn't seem right to me.
Any thoughts?
Colin Green
following is a bug somewhere in .Net (whether it be the CLR, JITter, C#
compiler). This is all in the context of .Net 1.1 SP1.
Recently we (my fellow team members & I) observed an
InvalidOperationException - 'collection has been modified', however it
wasn't immediately obvious why this would be occuring. There are no
modifications occuring within the loop and there is not another thread
that could be making a change.
The object being looped over was derived from CollectionBase and the
culprit turned out to be a finalizer on the class, a finalizer that
called Clear() thus modifying the collection. My question then is, why
did the finalizer get called while the CollectionBase object was still
in scope? Or more to the point, the object has clearly gone out of
scope and so should it have done?
Here's some code to demonstrate the issue fairly reliably (on my PC)...
---------------------------------------------
// Our problematic CollectionBase class.
public class TestCollection : CollectionBase
{
public void Add(object o)
{
List.Add(o);
}
~TestCollection()
{
this.Clear();
}
}
// A helper class. This has no real relevance to the problem.
public class ListItem
{
private string itemCode;
private string itemDescription;
public ListItem(string itemCode, string itemDescription)
{
this.itemCode = itemCode;
this.itemDescription = itemDescription;
}
public override string ToString()
{
return itemDescription;
}
}
---------------------------------------------
If you then create a form in Visual Studio, drop a ComboBox and a
button on there and add the following code:
--------------------------------------------
// Creates an instance of TestCollection with some ListItems(see above)
in it.
private TestCollection BuildCol(int size)
{
TestCollection oList = new TestCollection();
for(int i=0; i<size; i++)
{
string s = i.ToString();
oList.Add(new ListItem(s,s));
}
return oList;
}
// Populates our combobox.
private void PopulateCombobox()
{
TestCollection col = BuildCol(1000);
foreach(object o in col) // exception thrown here!
{
comboBox1.Items.Add(o);
}
comboBox1.Sorted = true;
}
// From the button click event we populate the combobox several times
over to
// increase the chances of reproducing the problem.
private void button1_Click(object sender, System.EventArgs e)
{
for(int i=0; i<100; i++)
{
PopulateCombobox();
comboBox1.Items.Clear();
}
}
--------------------------------------------
Running this code generates an exception on every clicks of the button
here. If you don;t get this then you can add a call to GC.Collect()
within the foreach loop to cause a garbage collection which in turn
will invoke the TestCollection's finalizer.
What I think is happening here is that foreach is just shorthand for
something like:
--------------
IEnumerator e = col.GetEnumerator();
while(e.Current!=null)
{
// Do stuff here.
e.MoveNext();
}
--------------
So although technically 'col' is in scope for the lifetime of the loop,
in reality a code optimizer may be flagging it as out of scope since it
is not actually being used within the loop. If a garbage collection
happens to occur then the finalizer is called, modifying the collection
and bang! Of course the IEnumerator would normally have a reference to
the collection so this really doesn't seem right to me.
Any thoughts?
Colin Green