Foreach
Intro
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();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
// Use item
}
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);
}
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
foreach?
Any type that implements IEnumerable / IEnumerable<T>, or any type that provides the enumerator pattern (GetEnumerator() + Current + MoveNext()).
foreach implemented under the hood?
The compiler lowers it to enumerator code: call GetEnumerator(), loop MoveNext(), read Current.
yield and how does it work?
It creates an iterator: each yield return produces a value and pauses the method; the method resumes on the next iteration request.
yield return instead of returning a materialized collection like List<T>?
Use yield return for 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 or Count, or repeated enumeration without rerunning expensive or side-effectful generation logic.
Links
- 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.