Bear with me as I'm doing "interesting stuff" here with enumerators and LINQ. And while checking if things get properly cleaned up, I noticed something within a simple console application that I created to test a few things. But first, some pieces of code, starting with the generic DoAll() method:
public static IEnumerable<T> DoAll<T>(this IEnumerable<T> data, Action<T> action)
{
foreach (var item in data)
{
action(item);
yield return item;
}
}
This just does a specific action with an item before yielding it to the next method in the pipeline. Next, a base IDisposable object that just serves as example:
public class Dummy : IDisposable
{
private static int _count = 0;
public int Count = ++_count;
public Dummy() => Console.WriteLine($"Dummy {Count} created.");
~Dummy() => Console.WriteLine($"Dummy {Count} destroyed.");
public void Dispose() => Console.WriteLine($"Dummy {Count} disposed.");
private int _value = 0;
public int Value => ++_value;
}
The Dummy is simply an object that counts how often one has been created and a value that keeps track of how often it was called. As I said, simple example. Now I need an enumerator! This one:
public static class DummyList
{
public static IEnumerable<int> GetDummy()
{
using (var dummy = new Dummy())
{
while (true) { yield return dummy.Value; }
}
}
}
No rocket science here. Just a static class with a static extension method that will call the Dummy Value method forever in a loop, yielding each value to the next method in the pipeline.
Just to be clear, while this seems an infinite loop, the loop will get broken in the next method. And the next method is a test method:
public static void DummyTest()
{
for (var x = 0; x < 5; x++)
{
Console.WriteLine(
string.Join(", ",
DummyList.GetDummy()
.DoAll(i => Console.Write($"Got {i}! "))
.Take(10)
.Select(i => i.ToString())));
}
GC.Collect();
}
Well, the above code will use my enumerator 5 times, each time taking just 10 values before writing it as a list to the console. And at the end of it all, I call the garbage collector to clean it up. And I just call DummyTest();
from my main method in my console application to get this result:
Dummy 1 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 1 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 2 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 2 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 3 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 3 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 4 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 5 created.
Got 1! Got 2! Got 3! Got 4! Got 5! Got 6! Got 7! Got 8! Got 9! Got 10! Dummy 5 disposed.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Dummy 4 destroyed.
Dummy 3 destroyed.
Dummy 2 destroyed.
Dummy 1 destroyed.
Well, it almost looks okay. Every time the object gets created and disposed so I know my enumeration will get cleaned up, even though I break it off during the loop. I wanted to make sure this would happen and I first wanted to ask here if it would. But this test shows it does.
However, the garbage collector only destroys 4 of the 5 objects! And that troubles me a bit. All this code to show my enumerator will clean up nicely, only to discover that it won't clear up everything. And while it's not that important, I do want to know why it won't delete the last object and how I can force the garbage collector to destroy them all in this complex example.
So why isn't the last object destroyed and how can I force it's destruction within the DummyTest method?
Things are getting a bit more complicated. This error only happens in "Debug" mode, not "Release" mode. I'm not debugging it when it skips Dummy 5, but I have compiled it with and without debug information. Without debug information Dummy 5 gets freed. Something seems to hold onto that Dummy when the project is compiled with debug information.
I want to know why, and how to prevent this.