Loops
Repeating work in C#: for, foreach, while and do-while loops, break and continue, looping over collections safely, and when a LINQ query is the clearer choice.
while and do-while
A loop runs the
same block again and again. The simplest is the
while loop: it tests a bool condition,
runs the block if it is true, and then tests again. When the
condition is false, the program continues after the loop. Use it
when you do not know in advance how many rounds you need.
decimal credit = 120.00m;
decimal monthlyCharge = 45.50m;
int months = 0;
while (credit >= monthlyCharge)
{
credit -= monthlyCharge;
months++;
Console.WriteLine($"Month {months}: credit left {credit:F2}");
}
Console.WriteLine($"Prepaid credit lasts {months} months.");
Month 1: credit left 74.50 Month 2: credit left 29.00 Prepaid credit lasts 2 months.
Something inside the loop must eventually make the condition false, here the decreasing credit. Otherwise you have an infinite loop. Stop a runaway console program with Ctrl+C.
do-while
A do … while loop tests its condition
after the block, so the block always runs at least once.
That fits "ask, then check" situations such as input validation.
Note the semicolon after the condition.
int reading;
string? input;
do
{
Console.Write("Meter reading (kWh): ");
input = Console.ReadLine();
}
while (!int.TryParse(input, out reading) || reading < 0);
Console.WriteLine();
Console.WriteLine($"Saved reading: {reading} kWh");
Meter reading (kWh): Meter reading (kWh): Meter reading (kWh): Saved reading: 4120 kWh
The user typed abc, -5 and
4120. Typed input does not appear in the output above,
so the three prompts end up on one line; in a real terminal each
answer appears after its prompt. int.TryParse returns
false for text that is not a number, which keeps the
loop going (see
out parameters
for how it hands back the number).
The for loop
When you know how many rounds you need, the
for loop keeps everything about the counting in one
line: an initializer that runs once, a condition that is
tested before every round, and an iterator that runs after
every round. This is the C# form of a
count-controlled loop.
for (int hour = 0; hour < 24; hour += 6)
{
Console.WriteLine($"{hour:D2}:00 collect meter data");
}
for (int seconds = 3; seconds > 0; seconds--)
{
Console.WriteLine($"Restarting in {seconds}...");
}
00:00 collect meter data 06:00 collect meter data 12:00 collect meter data 18:00 collect meter data Restarting in 3... Restarting in 2... Restarting in 1...
The counter hour exists only inside the loop. Any of
the three parts may be left out: for (;;) is an endless
loop, usually ended with break.
Looping over an array by index
A for loop is the right tool when you need the position
of each item, or need to visit only some positions. Arrays are
numbered from 0, so the last valid index is Length - 1:
the condition is i < readings.Length.
int[] readings = [412, 415, 419];
for (int i = 0; i <= readings.Length; i++)
{
Console.WriteLine($"Reading {i}: {readings[i]}");
}
Reading 0: 412 Reading 1: 415 Reading 2: 419 Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
foreach over collections
The
foreach statement
runs its block once for every item of a collection, in order,
without a counter. You declare the type and name of the
iteration variable, then in and the
collection. It works on arrays, lists, dictionaries, sets, strings
(one char at a time) and the result of a LINQ query.
string[] cities = ["Delft", "Leiden", "Gouda"];
foreach (string city in cities)
{
Console.WriteLine($"Scheduling meter visits in {city}");
}
int digits = 0;
foreach (char c in "INC-20481")
{
if (char.IsDigit(c))
{
digits++;
}
}
Console.WriteLine($"{digits} digits");
Scheduling meter visits in Delft Scheduling meter visits in Leiden Scheduling meter visits in Gouda 5 digits
You may write var instead of the item type (foreach (var city in cities)); the compiler infers it from the collection. Looping over a
dictionary gives you key-value pairs, shown on
arrays, lists and collections.
The iteration variable is read-only
The iteration variable is a fresh copy of each item, and C# does not let you assign to it. This prevents the mistaken belief that changing it would change the array.
int[] readings = [412, 415, 419];
foreach (int r in readings)
{
r = r + 1;
}
example.cs(5,5): error CS1656: Cannot assign to 'r' because it is a 'foreach iteration variable'
To change the items of an array, loop with for and
assign through the index: readings[i] += 1. If the
items are objects (instances of a
class), you can still change
their properties through the iteration variable, because it refers
to the same object.
Do not change a list while looping over it
Adding or removing items of a List<T> during a
foreach over that same list would leave the loop unsure
which item comes next. .NET detects it and throws on the next step:
List<string> tickets = ["INC-20481", "INC-20482", "INC-20483"];
foreach (string ticket in tickets)
{
if (ticket == "INC-20482")
{
tickets.Remove(ticket);
}
}
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
Three safe alternatives: let the list do it with
RemoveAll, loop backwards with
for (removing an item then never shifts an item you
still have to visit), or build a new list with the items you want to
keep.
List<string> tickets = ["INC-20481", "INC-20482", "INC-20483"];
int removed = tickets.RemoveAll(t => t == "INC-20482");
Console.WriteLine($"{removed} removed: {string.Join(", ", tickets)}");
List<int> readings = [412, -1, 419, -1, 431];
for (int i = readings.Count - 1; i >= 0; i--)
{
if (readings[i] < 0)
{
readings.RemoveAt(i);
}
}
Console.WriteLine(string.Join(", ", readings));
1 removed: INC-20481, INC-20483 412, 419, 431
What foreach needs: IEnumerable
foreach is not limited to built-in collections. It
works on any type that can hand out its items one at a time. In .NET
that ability is described by the
interface
IEnumerable<T>
(and its older, non-generic sibling IEnumerable): an
object that can give you an enumerator, which in turn moves
from item to item. Arrays, List<T>,
Dictionary<TKey,TValue> and strings all implement
it, and so does every LINQ query. A method that accepts an
IEnumerable<T> parameter therefore accepts all of
them.
You can write your own sequence with an iterator method: a
method that returns IEnumerable<T> and uses
yield return to hand out one item at a time. Like a
Python
generator function, it produces items lazily, only when the loop asks for the next
one.
foreach (DateOnly day in WorkingDays(new DateOnly(2026, 10, 1), 4))
{
Console.WriteLine($"{day:yyyy-MM-dd} {day.DayOfWeek}");
}
static IEnumerable<DateOnly> WorkingDays(DateOnly start, int count)
{
DateOnly day = start;
while (count > 0)
{
if (day.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday))
{
yield return day;
count--;
}
day = day.AddDays(1);
}
}
2026-10-01 Thursday 2026-10-02 Friday 2026-10-05 Monday 2026-10-06 Tuesday
break and continue
Two statements change the course of a loop from inside it, as in most C-family languages (see break and continue in general):
-
breakends the loop immediately; the program continues after it. -
continueskips the rest of the current round and starts the next one (in aforloop, the iterator still runs).
string[] queue = ["INC-20481", "", "INC-20482", "STOP", "INC-20483"];
foreach (string ticket in queue)
{
if (ticket == "")
{
continue; // skip empty lines
}
if (ticket == "STOP")
{
break; // end of today's batch
}
Console.WriteLine($"Processing {ticket}");
}
Console.WriteLine("Batch done.");
Processing INC-20481 Processing INC-20482 Batch done.
Nested loops
A loop inside a loop runs completely for every round of the outer
one. break only leaves the innermost
loop it is in:
string[] substations = ["North", "South"];
int[][] loads = [[62, 97, 71], [55, 58, 60]];
for (int s = 0; s < substations.Length; s++)
{
foreach (int load in loads[s])
{
if (load > 90)
{
Console.WriteLine($"{substations[s]}: overload {load}%, stop checking");
break;
}
Console.WriteLine($"{substations[s]}: {load}% ok");
}
}
North: 62% ok North: overload 97%, stop checking South: 55% ok South: 58% ok South: 60% ok
To leave several loops at once, put them in a
method and use
return, which is usually the clearest option. C# has no
labeled break like
JavaScript's
yet; labeled break and continue are listed
for C# 15, which is still in preview as of October 2026.
Indexes from the end and ranges
Loops often need "the last item" or "everything except the first".
C# 8 added two small operators for that, which work on arrays,
strings, List<T> and spans.
The
index from end operator
^ counts from the back: ^1 is the last
item, ^2 the one before it. So
readings[^1] means the same as
readings[readings.Length - 1], without repeating the
variable name. ^0 points just past the end and is never
a valid item.
The
range operator
.. selects a slice: start..end includes
start and excludes end, so
readings[1..3] has two items. Either side may be left
out (..2 = the first two, 2.. = from index
2 to the end) and either side may count from the end (^2..
= the last two).
1..3: it starts at index 1 and stops before index 3.
int[] readings = [412, 415, 419, 431, 440];
Console.WriteLine(readings[^1]);
Console.WriteLine(string.Join(" ", readings[1..3]));
Console.WriteLine(string.Join(" ", readings[^2..]));
// usage between consecutive readings: compare each with the previous one
foreach (int i in Enumerable.Range(1, readings.Length - 1))
{
Console.Write($"{readings[i] - readings[i - 1]} ");
}
Console.WriteLine();
string ticket = "INC-20481";
Console.WriteLine(ticket[4..]);
440 415 419 431 440 3 4 12 9 20481
A range on an array, string or list creates a new
array, string or list with copies of the items; changing the slice
does not change the original.
Enumerable.Range(start, count) produces
count numbers starting at start, which is
handy wherever a sequence of numbers is needed.
Loops or LINQ?
Many loops follow the same few shapes: filter some items, transform
them, add them up. For those shapes .NET offers
LINQ (Language Integrated Query), a set of
methods such as Where, Select and
Sum that work on every IEnumerable. Both
halves of the next example compute the same total:
decimal[] usageKwh = [310.5m, 512.0m, 98.25m, 640.0m];
// with a loop
decimal total = 0m;
foreach (decimal kwh in usageKwh)
{
if (kwh > 300m)
{
total += kwh;
}
}
Console.WriteLine(total);
// with LINQ
Console.WriteLine(usageKwh.Where(kwh => kwh > 300m).Sum());
// numbered items, like Python's enumerate()
foreach (var (index, kwh) in usageKwh.Index())
{
Console.WriteLine($"{index + 1}. {kwh} kWh");
}
1462.5 1462.5 1. 310.5 kWh 2. 512.0 kWh 3. 98.25 kWh 4. 640.0 kWh
The LINQ version says what you want rather than
how to compute it, and it is hard to get an off-by-one
error in it. A plain loop is the better choice when the body has
side effects (printing, writing files, calling a service), when you
need break, or when several results are built in one
pass. Index() (added in .NET 9) pairs each item with
its position, the C# counterpart of Python's
enumerate(). (The trailing .0 in 512.0 is not a
mistake: a decimal remembers how many decimals it was
written with.)
Choosing a loop
| Loop | Use it when |
|---|---|
foreach |
you want every item of a collection, in order (the most common case) |
for |
you count, need the index, skip positions, change array items, or walk backwards |
while |
the number of rounds depends on something that happens inside the loop |
do … while |
the block must run at least once, as in input prompts |
| LINQ | you filter, transform, sort or aggregate a collection |