I was reading this comment by casablanca in https://softwareengineering.stackexchange.com/a/411324/109967:
There's another caveat here: value types are boxed onto the heap if accessed via an interface, so you'd still incur a heap allocation if enumerating via IList or IEnumerable. You'd have to be holding onto a concrete List instance to avoid the allocation.
I wanted to test this theory using this .NET 5 console app (I've tried swapping between the List<T>
and IList<T>
):
using System;
using System.Collections.Generic;
namespace ConsoleApp10
{
class Program
{
static void Main(string[] args)
{
IList<int> numbers = new List<int> { 0, 1, 2, 3, 4, 5 };
//List<int> numbers = new List<int> { 0, 1, 2, 3, 4, 5 };
for (int i = 0; i < 2000; i++)
{
foreach (var number in numbers)
{
}
}
GC.Collect();
Console.WriteLine("GC gen 0 collection count = " + GC.CollectionCount(0)); //Always 1
Console.WriteLine("GC gen 1 collection count = " + GC.CollectionCount(1)); //Always 1
Console.WriteLine("GC gen 2 collection count = " + GC.CollectionCount(2)); //Always 1
}
}
}
It appears as though all generations get full and are GCed.
If List<T>
uses a struct Enumerator
, then there should not be any heap allocations happening up until GC.Collect()
is called (afterwards string objects are created in the calls to Console.WriteLine()
but that happens after the GC runs).
Question 1:
Why are heap allocations happening when using List<T>
?
Question 2:
I understand that there would be heap allocations due to the reference type Enumerator
used when iterating a List<T>
via the IList<T>
interface (assuming the comment in the linked question is correct), but why does a collection happen in all 3 generations?
2000 Enumerator objects is a lot of objects, but once the foreach loop completes, it's ready to be GCed because nothing is referring to the object after the foreach completes. Why are the objects making it through to Gen 1 and Gen 2?