Every way to loop over data
while, do, for, foreach, LINQ, yield return, recursion, await foreach and parallel loops. Which one says what you mean.
Pick the loop that says the most about what you're doing.
- Visit every item, in order →
foreach. It works on arrays, lists, dictionaries, strings and anything else that's a sequence, and there's no index to get wrong. - You need the index, you step by more than one, or you walk
backwards →
for. - You don't know how many passes (re-prompt until the input is
valid, read until the file ends) →
while. If the body must run at least once, like a menu,do ... while. - You're building a new sequence from an old one (keep the
paid orders, turn each into a line of text, total them) → LINQ:
Where,Select,Sum. CallToList()when you need the answer now and more than once. - You're producing items one at a time for someone else to
loop over → an iterator method with
yield return. - The data is nested (folders in folders, replies to replies) → recursion. For a flat run of steps, the loop is simpler and can't overflow the stack.
- Each item arrives asynchronously (pages from an API, rows
from a database) →
await foreachover anIAsyncEnumerable<T>. - Many independent, CPU-heavy items →
Parallel.ForEach, and only after you've measured that a plain loop is too slow.
Whatever you pick, don't add or remove items from a collection
while foreach is walking it.
The four loop statements
Section
while and do ... while
Check first, or run once and then check. Loops that never end, and how to spot them.
Section
for and foreach
A counter you control, or every item in turn. Off-by-one errors and loop-variable scope.
Section
break, continue and return
Leave the loop, skip one pass, or leave the whole method, and when a flag reads better.
Section
Looping over an array
for when you need the index, foreach when you don't, and why foreach can't change the elements.
Formal. A List<T>'s enumerator is invalidated by any change to
the list while a foreach loop is running over it, including
Add, Remove, and even replacing an item with list[i] = x; the
loop's next step throws InvalidOperationException ("Collection was
modified"). Dictionary<TKey,TValue> and HashSet<T> are more
permissive on modern .NET (since .NET Core 3.0): Remove and
overwriting the value of an existing key are allowed during
enumeration, but Add (and any change that adds a new key) still
throws.
In plain English. A foreach loop is walking through a collection.
If you add or remove items while it walks, it can lose its place, so
.NET stops the program instead of guessing. A list is strict about
this: change it in any way and the loop fails. A dictionary or set lets
you remove as you go, but adding new entries still fails.
Example. foreach (var n in numbers) if (n < 0) numbers.Remove(n);
throws on a List<int>. numbers.RemoveAll(...), a loop over a copy
([.. numbers]), or collecting the changes and applying them after
the loop all work.
Why this rule exists. Failing loudly at the first bad step is far better than silently skipping or repeating items.
Adding or removing items while foreach walks a list throws InvalidOperationException.
Sequences and LINQ
Anything foreach can walk is an IEnumerable
Section
Where and Select
Keep the items that pass a test, and turn each item into something else, without writing the loop.
Section
List methods that take a test
Find, FindAll, Exists and RemoveAll do the loop for you when you hand them a Predicate
Formal. Operators that return a sequence (Where, Select,
OrderBy, Take, GroupBy …) do no work when called; they return a
query object. The query runs when something enumerates it (foreach,
ToList, Count, First …), against the source as it is at that
moment, and it runs again in full on every enumeration, repeating any
side effects in its lambdas. Operators that return a single value or a
materialized collection (Count, Sum, First, Any, ToList,
ToArray, ToDictionary) execute immediately.
In plain English. Writing var adults = people.Where(p => p.Age >= 18);
doesn't find any adults yet. It writes down a recipe for finding them.
The recipe is followed only when you ask for the results, and it is
followed again each time you ask. Add a person to the list first, and
the next run includes them. Call ToList() when you want the answers
frozen at one moment.
Example. With var q = nums.Where(n => n > 1); over [1, 2, 3],
adding 4 to nums and then calling q.Count() gives 3, not 2.
Calling q.Count() twice runs the filter lambda over the list twice.
Why this rule exists. Deferring lets queries be composed cheaply,
stream large or endless sources, and stop early (Take, First)
without touching every item.
A query runs when you loop over it, not when you write it. ToList() runs it now.
Other shapes of repetition
Section
Recursion, or a loop?
A method that calls itself fits nested data. For a straight run of steps, the loop wins.
A loop whose items arrive over time, with cancellation and early exit.
Spread independent, CPU-heavy items across cores. Worth it only for real work and no shared state.