Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

The foreach statement is an iteration construct that enumerates the elements of a collection. It provides a forward-only traversal of any type that implements the System.Collections.IEnumerable, System.Collections.Generic.IEnumerable<T>, or System.Collections.Generic.IAsyncEnumerable<T> interfaces, or any type that satisfies the C# enumerable pattern.

Syntax

// Standard iteration
[await] foreach ([ref | ref readonly] Type iterationVariable in collection)
{
    // Statement(s) to execute
}

// Tuple deconstruction iteration
[await] foreach (var (element1, element2) in collection)
{
    // Statement(s) to execute
}
  • await: An optional keyword used for asynchronous iteration over collections implementing IAsyncEnumerable<T> or the asynchronous enumerable pattern.
  • Type: The data type of the elements in the collection. The implicitly typed local variable var can be used to instruct the compiler to infer the type.
  • iterationVariable: A local variable representing the current element in the iteration. By default, this variable is read-only. If the collection’s enumerator yields references, it can be declared with the ref or ref readonly modifier to allow direct mutation or avoid copying.
  • collection: The expression representing the iterable data structure.
  • Tuple Deconstruction: Introduced in C# 7.0, the foreach loop supports deconstructing tuples or types with a Deconstruct method (such as KeyValuePair<TKey, TValue>) directly in the iteration variable declaration.

Compiler Expansion (Under the Hood)

The foreach loop is syntactic sugar. At compile time, the C# compiler translates the foreach statement into lower-level constructs based on the type of the collection being iterated.

Array Optimization

When iterating over a single-dimensional, zero-based array (SZArray), the compiler completely bypasses GetEnumerator() and IEnumerator. To maximize performance and avoid allocation, it expands the foreach statement into a standard for loop using an indexer:
// Original foreach
int[] numbers = { 1, 2, 3 };
foreach (int item in numbers)
{
    Console.WriteLine(item);
}

// Compiler expansion
int[] tempArray = numbers;
for (int i = 0; i < tempArray.Length; i++)
{
    int item = tempArray[i];
    Console.WriteLine(item);
}

Enumerator Expansion

For non-array collections, the compiler translates the foreach statement into a while loop that explicitly manages the enumerator. Crucially, the compiler binds to the exact type returned by GetEnumerator(). It does not cast the enumerator to IEnumerator<T> or IEnumerator. Many Base Class Library (BCL) collections (such as List<T> or Span<T>) return a struct enumerator. By binding to the exact struct type, the compiler avoids boxing allocations that would occur if the struct were cast to an interface.
// Conceptual compiler expansion for a collection
var enumerator = collection.GetEnumerator(); 
try
{
    while (enumerator.MoveNext())
    {
        var item = enumerator.Current;
        Console.WriteLine(item);
    }
}
finally
{
    // The compiler generates disposal logic based on the enumerator's compile-time type.
    // It avoids boxing value types by using the `constrained.` IL prefix.
}
Disposal Resolution Rules: The compiler optimizes the finally block based on the enumerator’s compile-time type:
  1. Compile-time IDisposable: If the enumerator type explicitly implements IDisposable (like IEnumerator<T>), the compiler generates a direct call to Dispose(). For value types, it emits the constrained. IL instruction to invoke Dispose() without boxing the struct.
  2. Pattern-Based Disposal (ref struct): Because ref struct types cannot implement interfaces, C# 8.0 and later use pattern matching for disposal. If the ref struct enumerator has a public void Dispose() method, the compiler generates a finally block to invoke it directly.
  3. Compile-time Non-Disposable: If the enumerator is a struct or a sealed class that neither implements IDisposable nor has a pattern-based Dispose() method, the compiler knows at compile time that it cannot be disposed. It completely omits the finally block.
  4. Runtime Check Required: If the enumerator is an unsealed class or an interface (like the non-generic IEnumerator) where the implementation is unknown at compile time, the compiler generates a runtime type check (conceptually if (enumerator is IDisposable disposable)).

The Enumerable Pattern (Duck Typing)

C# does not strictly require a collection to implement IEnumerable or IEnumerable<T> for foreach to function. The compiler uses pattern matching (duck typing) to resolve the iteration. A type can be iterated over if it satisfies the following patterns: Synchronous Pattern (foreach):
  1. It contains a public method named GetEnumerator() that can be invoked with no arguments. This requirement is satisfied by a parameterless method, a method where all parameters have default values (optional parameters), or an extension method.
  2. The return type of GetEnumerator() has a public method named MoveNext() that can be invoked with no arguments (including methods utilizing optional parameters) and returns a bool.
  3. The return type of GetEnumerator() has a public property named Current equipped with a get accessor.
Asynchronous Pattern (await foreach):
  1. It contains a public method named GetAsyncEnumerator() that can be invoked with no arguments (including methods with optional parameters or extension methods).
  2. The return type of GetAsyncEnumerator() has a public method named MoveNextAsync() that can be invoked with no arguments (including methods with optional parameters). The return type of MoveNextAsync() must be an awaitable type whose awaiter yields a bool result (such as ValueTask<bool>, Task<bool>, or a custom awaitable type).
  3. The return type of GetAsyncEnumerator() has a public property named Current equipped with a get accessor.

Technical Constraints and Features

  • Iteration Variable Mutability: By default, the iterationVariable is read-only. Attempting to reassign a standard by-value iteration variable will result in compiler error CS1656. However, if the enumerator’s Current property returns by reference (ref T), the iteration variable can be declared with the ref modifier (e.g., foreach (ref int item in span)). This allows direct reassignment and mutation of the underlying collection’s elements.
  • Collection Modification: The underlying enumerator tracks the version or state of the collection. If the collection is structurally modified (elements added, removed, or reallocated) during the foreach execution, the next call to MoveNext() will typically throw an InvalidOperationException.
  • Asynchronous Iteration: For collections implementing IAsyncEnumerable<T> or the asynchronous enumerable pattern, the await foreach variant asynchronously retrieves the next element, yielding control back to the calling thread while waiting for data to materialize. The compiler expands this into a state machine utilizing IAsyncEnumerator<T> and DisposeAsync().
Master C# with Deep Grasping Methodology!Learn More