Course contentsShow
← C#
Lesson 304 of 1,9083. Generics and Collections ArchitectureFree lesson

Struct-based Collection Iterators

Design struct iterators to enable heap-free collection traversal.

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.