C# example

How to loop with an index in C#

6 min read Updated Oct 2026 Runs in an isolated runtime
Quick answer

On .NET 9+, call Index() and deconstruct the pair: foreach (var (i, item) in items.Index()). On older versions, use items.Select((item, i) => (item, i)), or a plain for (int i = 0; i < items.Count; i++) loop when you have an array or a List<T>.

foreach has no built-in counter: it walks an IEnumerator, which only knows how to move to the next element, not where it is. So the index has to come from somewhere else. .NET 9 added the LINQ method Index() for exactly this; before that, the usual answers were the index overload of Select, a classic for loop or a counter you increment yourself. This page shows each one, the two bugs people hit with hand-rolled counters, and Zip for the common case where the index only exists to line up two collections. Each example runs on this page: hit Run, then edit the code and run it again.

1foreach over Index() (.NET 9+)Recommended

Enumerable.Index() turns any IEnumerable<T> into a sequence of (int Index, T Item) tuples. Deconstruct the tuple in the loop header and you get two named variables, with the index starting at 0. Because it works on the enumerable rather than on an indexer, it covers lists, arrays, strings, sets, LINQ queries and anything else foreach can walk, and break and continue work as usual.

Program.cs

Output

Prints 0: apple, 1: banana, 2: cherry, then the same list numbered 1. apple to 3. cherry. The string loop finds digit '1' at 3 and digit '4' at 4, and the break loop prints first b-fruit at index 1. The last two loops show that position in the chain matters: Index() before Where gives original index 1: 95 and original index 3: 88, while Index() after it renumbers the survivors as filtered index 0: 95 and filtered index 1: 88. Without deconstruction, the tuple fields are named Index and Item, as in p.Item above.

2Select((item, i) => …) on older .NET

Before .NET 9, the standard trick was the overload of Select whose lambda takes the element and its index. Project each pair into a tuple and deconstruct it in the foreach, and you have the same loop as Index(), just with the values in the order you choose. Where, SelectMany, TakeWhile and SkipWhile have the same kind of overload, so often you don't need a loop at all.

Program.cs

Output

Prints 0: C# through 3: C++, then 1) C# 2) F# 3) VB 4) C++ from the projection, C#, VB from the even-position filter, and 0 -> C# through 3 -> C++ from the anonymous-type version. Tuple deconstruction in foreach needs C# 7; on any older code base you will see the new { value, index } form instead, which allocates an object per element. Both add a delegate call per element, which only matters in hot loops; see the FAQ for when to prefer for.

3A plain for loop over an array or List<T>

When the collection has an indexer, a classic for loop is the most direct way to have the index: it is the loop variable. It is also the only option on this page that lets you write elements back, go backwards, skip by more than one or look at the neighbouring element. Arrays expose Length; List<T> and other collections expose Count.

Program.cs

Output

Prints day 0: 18 to day 4: 23, then the updated array 19, 22, 26, 20, 24, then 24 20 26 22 19 backwards and [0]=19 [2]=26 [4]=24 in steps of two. The neighbour loop prints day 1: +3, day 2: +4, day 3: -6 and day 4: +4, and the backwards removal leaves 1, 3, 5. Counting up instead skips the element that slides into the freed slot: the same loop run forwards over 1, 2, 4, 5, 6 leaves 1, 4, 5. Writing list[i] = … from inside a foreach over the same list doesn't work either: List<T> treats it as a modification and throws on the next step (see the FAQ). The [18, 21, 25, 19, 23] collection expressions need C# 12 (.NET 8); on older versions write new[] { 18, 21, 25, 19, 23 } or new List<int> { 1, 2, 3 }.

4A manual counter, and two traps

Declaring int index = 0; before a foreach and incrementing it at the end of the body works everywhere and needs nothing from LINQ. It has two classic failure modes. A continue jumps past an increment at the bottom of the body, so the counter falls behind. And looking the index up with IndexOf(item) instead of counting returns the position of the first equal element, which is wrong for duplicates and turns the loop into O(n²).

Program.cs

Output

The plain counter prints 0: red to 3: blue. The drifting one prints drifted 0: red, drifted 1: red, drifted 2: blue: the second red really sits at index 2 and blue at 3. Moving the increment to the top fixes it: correct 0: red, correct 2: red, correct 3: blue. The IndexOf loop prints IndexOf(red) = 0 twice, so the second red is reported at the wrong position. Index() and for have neither problem, which is why they come first on this page.

5Two collections by position: Zip

A lot of index loops exist only to read names[i] and scores[i] together. Zip does that pairing for you and hands back tuples you can deconstruct. Since .NET 6 it accepts a third sequence, and on .NET 9 you can chain Index() after it when you still want the position. The one thing to know is that it stops at the end of the shorter sequence, silently.

Program.cs

Output

Prints alice: 91, bob: 78, carol: 85, then the three-way rows alice 91 A, bob 78 C, carol 85 B, then #1 alice to #3 carol. The nested pattern var (i, (name, _)) unpacks the index and the inner pair in one go, and _ discards the score. Zipping three names with two scores gives pairs: 2, and carol simply disappears; the for loop with an explicit bound keeps her and prints 2: carol missing.

6Which should you use?

MethodWorks onCan write elementsBest for
foreach (var (i, x) in items.Index())Any IEnumerable<T>NoThe default on .NET 9+
items.Select((x, i) => (x, i))Any IEnumerable<T>NoThe same loop on .NET 8 and older
for (int i = 0; i < n; i++)Arrays, List<T>, indexersYesWriting back, backwards, steps, neighbours, hot loops
int i = 0; foreach … i++Any IEnumerable<T>NoOld code; increment first if you use continue
list.IndexOf(x) inside foreachLists and arraysNoAvoid: wrong for duplicates, O(n²)
a.Zip(b)Any IEnumerable<T>NoPairing two or three sequences by position

Frequently asked questions

How do I get the index of the current iteration in a C# foreach loop?

On .NET 9 or later, loop over items.Index() and deconstruct each pair: foreach (var (i, item) in items.Index()). On older versions, use foreach (var (item, i) in items.Select((item, i) => (item, i))), or a for loop if the collection is an array or a List<T>. A counter declared before the loop and incremented in the body also works, as long as nothing skips the increment.

Why doesn’t foreach have a built-in index in C#?

Because foreach works on any IEnumerable<T>, and an enumerator only offers MoveNext() and Current. Many sequences have no meaningful position to report (a HashSet<T>, a database query, a generator method using yield return), so the language leaves it out. Index() adds the count on top of the enumerator, exactly as a manual counter would.

Can I change list elements inside a foreach using the index?

Not on a List<T>. Assigning list[i] = x * 10 inside foreach (var (i, x) in list.Index()) throws InvalidOperationException: Collection was modified; enumeration operation may not execute. on the next step, after the first element has already changed (the list ended up as 10, 2, 3). The same loop over an array works and gives 10, 20, 30, because arrays have no modification check. Assigning to the iteration variable itself does not compile: error CS1656: Cannot assign to 'w' because it is a 'foreach iteration variable'. Use a for loop to write elements back.

Is a for loop faster than foreach with Index()?

Over an array or a List<T>, usually yes. A for loop reads the elements straight through the indexer, while Index() and Select((x, i) => ...) go through the IEnumerable<T> interface and create a tuple per element. For ordinary code the difference is too small to matter, so pick the version that reads best; switch to for in measured hot paths.

Why shouldn’t I use IndexOf to get the index inside a foreach?

IndexOf searches from the start for the first equal element. With duplicates it returns the wrong position: in ["red", "green", "red", "blue"] both reds report index 0. It also searches the list once per iteration, making the loop O(n²). Use Index(), a for loop or a counter instead.

How do I start the index at 1 instead of 0?

Index() has no start parameter, so add 1 where you use it: Console.WriteLine($"{i + 1}. {item}") prints 1. apple, 2. banana and so on. The same applies to Select((item, i) => ...). With a for loop you can also start at i = 1, but then remember that the element is items[i - 1].

Can I use Index() on a Dictionary?

Yes, it works on any IEnumerable<T>, so foreach (var (i, (key, value)) in dict.Index()) gives a position plus the key and value (a SortedDictionary with keys x and y printed 0: x=1 and 1: y=2). The index is only a running count, though: a Dictionary<TKey, TValue> does not guarantee its enumeration order, so do not treat the number as a stable position for a key.

Run it yourself

Open any of these in the full C# editor: tweak, run, and share.

C# playground