How to Iterate Over Two Objects in C# Without Heap Allocations
When writing high-performance C# code, avoiding unnecessary garbage collection (GC) pressure is crucial. A common scenario arises when you have two separate variables referencing managed objects (e.g., p1 and p2) and you want to execute the exact same sequence of operations on both.
The standard lazy approach—creating an array on the fly—causes a heap allocation:
// Allocates a temporary Person[] array on the heap!
foreach (Person p in new Person[] { p1, p2 })
{
p.DoSomething();
p.DoMoreStuff();
}Why Standard Alternatives Fail
Before diving into the optimal solutions, let's address why common C# performance tricks don't work here out of the box:
stackalloc: You cannot usestackallocon managed types like class references (e.g.,stackalloc Person[]is invalid in C#).- Tuples: Syntax like
foreach (var p in (p1, p2))fails natively because C#ValueTupledoes not implement an enumerator. - Unrolling the loop: Code duplication leads to maintenance nightmares, especially when the operations performed inside the loop grow complex.
Solution 1: Custom Tuple Enumerator using ref struct (Best Syntax)
C# allows pattern-based foreach loops. As long as a type has a public GetEnumerator() method returning a struct with a MoveNext() method and a Current property, you can use foreach on it.
By defining a ref struct enumerator as an extension method for two-element tuples, you can achieve the exact syntax you want with zero heap allocations:
public static class ValueTupleExtensions
{
public static TwoItemEnumerator<T> GetEnumerator<T>(this (T Item1, T Item2) tuple)
{
return new TwoItemEnumerator<T>(tuple.Item1, tuple.Item2);
}
}
public ref struct TwoItemEnumerator<T>
{
private readonly T _item1;
private readonly T _item2;
private int _index;
public TwoItemEnumerator(T item1, T item2)
{
_item1 = item1;
_item2 = item2;
_index = -1;
}
public bool MoveNext()
{
_index++;
return _index < 2;
}
public readonly T Current => _index == 0 ? _item1 : _item2;
}
Now you can iterate directly over a C# tuple with zero allocation overhead:
foreach (Person p in (p1, p2))
{
p.DoSomething();
p.DoMoreStuff();
}Solution 2: C# 12 Collection Expressions with ReadOnlySpan<T>
If you are using C# 12 and .NET 8 or newer, compiler magic allows stack-allocating temporary buffers for ReadOnlySpan<T> using collection expressions. The compiler automatically optimizes array creation behind the scenes by utilizing inline arrays on the stack:
ReadOnlySpan<Person> persons = [p1, p2];
foreach (Person p in persons)
{
p.DoSomething();
p.DoMoreStuff();
}This approach requires no extra boilerplate code and compiles down to zero heap allocations.
Solution 3: Zero-Dependency Index Loop
If you prefer not to write helper extension methods and are on an older version of C#/.NET, an index-based loop using a ternary operator is fully allocation-free:
for (int i = 0; i < 2; ++i)
{
Person p = i == 0 ? p1 : p2;
p.DoSomething();
p.DoMoreStuff();
}Summary
For modern C# 12+ projects, utilize collection expressions with ReadOnlySpan<T>. For older framework versions where clean syntax is desired, implementing a custom ref struct extension on ValueTuple yields clean, unrolled-like performance without any GC impact.