foreach is the most common way to iterate a sequence in C#. It works with types that provide an enumerator (typically via IEnumerable / IEnumerable<T>), and the compiler rewrites the loop into an enumerator-based pattern.
You can use foreach with:
- Any type that implements
System.Collections.IEnumerableorSystem.Collections.Generic.IEnumerable<T> - Or any type that satisfies the enumerator pattern:
- A public parameterless
GetEnumerator()method (instance or extension) - The returned enumerator has a public
Currentproperty and a public parameterlessMoveNext()method returningbool
- A public parameterless
Internals (compiler lowering):
- For many built-in collections, the compiler emits code that calls
GetEnumerator(), then loops whileMoveNext()returnstrueand readsCurrent. - For some cases (for example, arrays), the compiler can optimize into an index-based loop.
Under the hood, foreach compiles into:
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
// Use item
}
}
finally
{
// If the enumerator is IDisposable, foreach disposes it — even on early break/exception.
(enumerator as IDisposable)?.Dispose();
}The finally/Dispose is the part people forget: it’s what runs the finally block inside an iterator method (and disposes a DbDataReader, file enumerator, etc.) when you break out of a loop early. Hand-rolling the while (MoveNext()) form skips this cleanup.
Struct enumerators and boxing. List<T>, arrays, and Span<T> expose a struct enumerator, so foreach over them allocates nothing (the JIT also elides bounds checks on Span<T>/arrays). But if you access the collection through IEnumerable<T>, GetEnumerator() returns the enumerator boxed to the interface — reintroducing the allocation. Iterate the concrete type on hot paths.
Iterators and yield
yield return and yield break let you write iterator methods: methods that produce a sequence lazily, one element at a time.
An iterator method returns IEnumerable<T> / IEnumerable (or in async scenarios IAsyncEnumerable<T>), but it doesn’t execute immediately:
- Calling the method creates an iterator object.
- Iteration starts when the consumer begins enumerating (often via
foreach). - Each
yield returnproduces the next element and suspends execution until the next element is requested. yield breakends the sequence early.
Example:
public static IEnumerable<int> CountNumbers(int start, int end)
{
for (int i = start; i <= end; i++)
{
yield return i;
}
}
foreach (var number in CountNumbers(1, 5))
{
Console.WriteLine(number);
}Two iterator gotchas:
- Deferred execution moves exceptions. Because the body doesn’t run until enumeration starts, an argument check inside an iterator method throws when the caller starts iterating, not at the call site. For eager validation, split a public wrapper (validate, then return) from a private iterator (
yield). yieldcan’t live inside atrywith acatch(onlytry/finallyis allowed), and not inside alock/unsafeblock. If you need catch semantics around yielded work, wrap the consumption, not the production.
Pitfalls
Modifying the collection during iteration: Mutating a List<T> or Dictionary<TKey,TValue> inside a foreach over it throws InvalidOperationException. The enumerator tracks an internal version counter and detects the change. Fix by iterating a snapshot (collection.ToList()) or collecting mutations in a separate list and applying them after the loop.
Closure variable capture: Lambdas created inside foreach capture the loop variable by reference unless you shadow it into a local. Before C# 5, all lambdas in a foreach could close over the same item variable and see only the last value. Best practice: always copy to a local inside the lambda body if lifetimes extend beyond the iteration: var current = item; tasks.Add(() => Process(current));.
Tradeoffs
foreachvsfor:foreachis idiomatic and works on any enumerable.foris faster on arrays andList<T>because the JIT can eliminate bounds checks when iterating from 0 toCount. Useforin tight inner loops over known-length collections;foreacheverywhere else.foreachvs LINQ: LINQ chains are composable and readable but allocate per-operator.foreachover the source directly allocates less. Use LINQ for readability on non-hot paths;foreach(orSpan<T>) for hot paths where allocation matters.foreachvsSpan<T>iteration: for CPU-bound loops over memory you own, iterating aSpan<T>orReadOnlySpan<T>eliminates enumerator overhead and enables bounds-check elimination. Typical microbenchmark improvement is 20–30% on large arrays.
Questions
What types can you use in
foreach?Any type that implements
IEnumerable/IEnumerable<T>, or any type that provides the enumerator pattern (GetEnumerator()+Current+MoveNext()).
How is
foreachimplemented under the hood?The compiler lowers it to enumerator code: call
GetEnumerator(), loopMoveNext(), readCurrent.
What is
yieldand how does it work?It creates an iterator: each
yield returnproduces a value and pauses the method; the method resumes on the next iteration request.
Why and when should you use
yield returninstead of returning a materialized collection likeList<T>?Use
yield returnfor deferred execution and streaming when consumers may stop early or the sequence is large, because it lowers peak memory usage. Materialize (ToList()/ToArray()) when you need a snapshot, random access orCount, or repeated enumeration without rerunning expensive or side-effectful generation logic.
References
- Iteration statements (foreach) — language reference for
foreachsyntax, duck-typing pattern, and async enumeration. - C# language specification: iteration statements — formal spec defining the enumerator pattern the compiler targets.
- yield statement (yield return / yield break) — reference for iterator methods, state machine semantics, and async iterators.
- Iterators (overview) — conceptual guide covering lazy evaluation, deferred execution, and
IAsyncEnumerable<T>. - How is foreach implemented in C#? (StackOverflow) — community explanation of compiler lowering with IL examples.
- Yield: what, where, and why (Habr) — Russian-language practitioner deep-dive into iterator state machines.