Loops
C# provides four loop forms: for, foreach, while, and do … while. The foreach loop iterates over any type that satisfies the enumerable pattern — implements IEnumerable<T>, exposes a GetEnumerator() method, or (since C# 9) is supported by an extension method. Iterators — methods that use yield return — are the C# mechanism for producing sequences lazily; async iterators (C# 8) extend the mechanism to asynchronous sources. LINQ provides a substantial alternative to explicit loops for many iteration patterns.
while
The while loop tests its controlling expression before each iteration:
while (!queue.IsEmpty) {
var item = queue.Dequeue();
Process(item);
}
The construct is the appropriate one for iteration whose termination condition is checked at the start, and where the number of iterations is not known in advance. Reading from a stream, polling for state, and graph traversal are typical.
The controlling expression must be of type bool (no implicit conversion from integer or pointer):
while (HasMoreData()) {
/* ... */
}
while (input != null) {
input = Reader.ReadLine();
if (input != null) Process(input);
}
do … while
The do … while loop tests its controlling expression after each iteration; the body always executes at least once:
int input;
do {
Console.Write("> ");
input = Console.Read();
} while (input < '0' || input > '9');
The construct is appropriate for iterations whose first execution is unconditional. It is less common than while in C# code; the principal uses are input prompts and certain numerical methods.
The terminating semicolon after while (cond) is a syntactic peculiarity; it is the only compound statement in C# that requires one.
The C-style for
The C-style for combines initialisation, test, and step:
for (int i = 0; i < n; i++) {
Process(data[i]);
}
The header has three parts:
- The init-statement (an expression statement or a declaration), evaluated once before the loop.
- The condition, evaluated before each iteration; the loop terminates when it is
false. - The iteration-expression, evaluated after each iteration.
Any of the three may be omitted; for (;;) is the conventional infinite loop.
The init declaration scopes the variable to the loop:
for (int i = 0; i < n; i++) {
/* i is in scope */
}
// i is not in scope here
The C-style for is conventional in C# for index-based iteration (when the index matters) and for non-trivial step patterns. For “iterate over a collection” the conventional choice is foreach.
Multi-variable for
The comma operator does not exist in C# (the , in for (a, b; c; d, e) is a syntactic separator, not the C operator). Multi-variable for is admitted but with the comma between init declarations:
for (int i = 0, j = n - 1; i < j; i++, j--) {
Swap(ref data[i], ref data[j]);
}
The multi-variable form is occasionally used for two-pointer or reverse-iteration patterns.
foreach
foreach is the conventional loop for iterating over collections:
foreach (int v in values) {
Process(v);
}
foreach (var (key, value) in dictionary) { // structured binding (C# 7)
Console.WriteLine($"{key}: {value}");
}
The element variable’s type may be specified explicitly (int v) or inferred (var v). var is conventional when the type is obvious from the source:
foreach (var person in people) { /* person is Person */ }
foreach (var (k, v) in dict) { /* destructured */ }
The enumerable pattern
foreach accepts any type that satisfies the enumerable pattern:
- The type has a
GetEnumerator()method (or an extension method, since C# 9). - The returned enumerator has a
MoveNext()method returningbool. - The enumerator has a
Currentproperty.
The pattern admits any of:
- A type implementing
IEnumerable<T>(the conventional case). - A type implementing
IEnumerable(the non-generic predecessor). - A type with
GetEnumerator()defined directly (not requiring the interface). - A type for which an extension method
GetEnumerator()is in scope (C# 9).
The pattern-based admission lets foreach work with types that for performance reasons cannot implement IEnumerable<T> (which boxes value-type enumerators); Span<T> is one example.
Modification during iteration
A foreach over a List<T> (or most collections) may not modify the underlying collection during the iteration; doing so throws InvalidOperationException on the next step. The conventional alternative is:
- Build a separate list of items to add or remove, then apply after the loop.
- Iterate via index in a
forloop. - Use
RemoveAll(Predicate<T>)orLINQ’sWhere(...).ToList().
items.RemoveAll(item => item.IsExpired);
// or:
var alive = items.Where(i => !i.IsExpired).ToList();
items.Clear();
items.AddRange(alive);
break and continue
break exits the innermost enclosing for, foreach, while, do, or switch:
foreach (var item in items) {
if (item.Matches(target)) {
found = item;
break;
}
}
continue skips the rest of the current iteration and proceeds to the next:
foreach (var item in items) {
if (item.IsHidden) continue;
Process(item);
}
C# does not have multi-level break or continue. Exiting a nested loop requires a flag, a goto, or a refactoring into a function:
// With goto:
foreach (var row in matrix) {
foreach (var v in row) {
if (v == target) goto found;
}
}
found:
// With a function:
bool FindInMatrix(int target, out int row, out int col) {
for (int i = 0; i < matrix.Length; i++) {
for (int j = 0; j < matrix[i].Length; j++) {
if (matrix[i][j] == target) {
row = i; col = j;
return true;
}
}
}
row = col = -1;
return false;
}
The function form generalises better; the goto form is occasionally clearer.
Iterators and yield return
C# 2 introduced iterators — methods that produce a sequence of values lazily through yield return:
IEnumerable<int> Range(int start, int count) {
for (int i = 0; i < count; i++) {
yield return start + i;
}
}
foreach (int n in Range(10, 5)) {
Console.Write($"{n} ");
}
// 10 11 12 13 14
The compiler synthesises a state machine that implements IEnumerable<int> and IEnumerator<int>; the method’s body is split into resumable segments at each yield return. Each call to MoveNext() on the enumerator runs the body up to the next yield return, suspends, and returns control.
yield break terminates the iteration:
IEnumerable<string> ReadLines(StreamReader reader) {
while (true) {
string? line = reader.ReadLine();
if (line is null) yield break;
yield return line;
}
}
The construction is the conventional way to write generators that produce sequences without materialising them into lists. LINQ’s deferred operators (Where, Select, etc.) are implemented using iterators internally.
Async iterators and await foreach
C# 8 introduced IAsyncEnumerable<T> and the corresponding await foreach:
async IAsyncEnumerable<string> ReadLinesAsync(string path) {
using var reader = new StreamReader(path);
while (await reader.ReadLineAsync() is { } line) {
yield return line;
}
}
await foreach (var line in ReadLinesAsync("data.txt")) {
ProcessLine(line);
}
The producer combines yield return with await; the consumer combines await with foreach. The combination is the conventional way to express asynchronous streams (network responses, paginated APIs, large file reads).
The iteration may be cancelled by passing a CancellationToken:
await foreach (var line in ReadLinesAsync(path).WithCancellation(token)) {
ProcessLine(line);
}
LINQ as an alternative
A substantial fraction of explicit loops in C# code are replaceable by LINQ:
// Explicit:
int total = 0;
foreach (var item in items) {
if (item.IsActive) total += item.Value;
}
// LINQ:
int total = items.Where(i => i.IsActive).Sum(i => i.Value);
The full treatment is in LINQ. The conventional contemporary advice:
- Use
foreachfor simple iteration where the body has side effects. - Use LINQ for filter, map, reduce, group, and sort operations.
- Use the C-style
foronly when the index is genuinely needed. - Use iterators for custom sequences that should be lazy.
Common iteration patterns
Index and value together
for (int i = 0; i < items.Count; i++) {
Process(i, items[i]);
}
// With LINQ:
foreach (var (item, index) in items.Select((it, i) => (it, i))) {
Process(index, item);
}
// (No built-in enumerate-with-index in C#'s base library.)
The C# library does not include a built-in enumerate (such as Python’s). The conventional substitutes are the indexed for or the LINQ projection above.
Iterating two collections in parallel
foreach (var (a, b) in first.Zip(second)) {
Process(a, b);
}
Zip (LINQ) produces pairs from two sequences, stopping at the shorter one.
Chunking
foreach (var chunk in items.Chunk(100)) {
ProcessBatch(chunk);
}
Chunk (.NET 6+) splits a sequence into arrays of a given size.
Reading lines from a file
foreach (var line in File.ReadLines(path)) {
ProcessLine(line);
}
File.ReadLines is a deferred (iterator-based) API: it reads lines on demand, not all at once. For asynchronous reading, the await foreach pattern with a custom async iterator (or File.ReadLinesAsync in .NET 7+) is the conventional choice.
Asynchronous loop with cancellation
await foreach (var item in source.WithCancellation(token)) {
if (token.IsCancellationRequested) break;
await ProcessAsync(item, token);
}
The pattern threads cancellation through both the enumeration source and the per-iteration work.