In this lesson, you will learn how to bypass the garbage collector during collection traversal by implementing the enumerator pattern using a struct instead of a class.
The Problem: Heap Allocation
When you implement IEnumerable<T>, the default approach often involves returning a class that implements IEnumerator<T>. Every time a user calls GetEnumerator(), the runtime creates a new object on the managed heap. In high-performance systems—like game engines or low-latency trading platforms—this creates constant "GC pressure," forcing the garbage collector to pause your application to clean up these short-lived objects.
The Solution: Struct Iterators
Because a struct is a value type, it is allocated on the stack rather than the heap. By defining your enumerator as a public struct, you allow the compiler to traverse your collection without creating any temporary objects.
To use this, you must define a custom GetEnumerator() method that returns your struct type directly, rather than returning the IEnumerator<T> interface:
public struct MyListEnumerator<T>
{
private readonly T[] _items;
private int _index = -1;
public MyListEnumerator(T[] items) => _items = items;
public bool MoveNext() => ++_index < _items.Length;
public T Current => _items[_index];
}
Avoiding Boxing
Crucially, you should not explicitly implement the IEnumerator<T> interface on your struct. If you do, C# will "box" the struct (move it to the heap) whenever you pass it to code expecting that interface.
Instead, provide a GetEnumerator() method that returns your struct by value. C#’s foreach loop is smart: if it finds a GetEnumerator() method (via duck typing), it will use your struct-based enumerator directly. This enables "heap-free" iteration, where the entire traversal process happens on the stack, leaving the garbage collector completely uninvolved.
This pattern is exactly how high-performance collections like List<T> and Span<T> maintain their speed—by ensuring that the act of "looking" at the data doesn't require "allocating" for the looker.
Key Takeaway: By implementing a custom struct enumerator and ensuring foreach can access it without interface casting, you eliminate heap allocations and keep your collection traversal zero-GC.