RefactorMCP catalog

Convert Foreach to For

Turns a foreach over an indexable collection into an index loop that reads each element into the loop's variable: foreach (var item in items) becomes for (int i = 0; i < items.Count; i++) with var item = items[i]; as the first statement of the body. The reverse of Convert For to Foreach.

Target

The loop, by a caret anywhere in it. A caret inside nested loops picks the innermost foreach.

Precondition

  • The collection has an int indexer and an int Length or Count, as arrays, strings, spans, List<T>, IList<T> and IReadOnlyList<T> do, and the indexer returns the elements the foreach yields.
  • The collection is a local, parameter, field or property, possibly through this, since the loop's condition reads it on every iteration.
  • The loop is not an await foreach.
  • A given index name is not already declared or used in the loop's scope.

Transformation

  • The header becomes for (int i = 0; i < collection.Length; i++), with Count for collections that have no Length.
  • The loop's variable is declared as the body's first statement, with the type the foreach wrote, from collection[i]. When the foreach converted each element explicitly, as foreach (int n in doubles) does, the value is cast.
  • A deconstructing foreach (var (a, b) in pairs) deconstructs pairs[i] instead.
  • A body without braces gets a block.
  • The index is arguments.name, or the first of i, j, k and index that is not declared or used in the loop's scope.

Preserved

  • Behaviour for collections not changed during the loop: the same elements in the same order, each in a fresh variable, so lambdas that capture it still see their own element.
  • Comments above the loop, after its header and in its body.

Limitations

  • A collection computed by an expression is refused rather than stored in a local first.
  • A body that changes the collection, which the foreach would have stopped with an exception, runs on over the changed collection.
  • The refactoring changes one loop, so there are no references in other files to update.

Error codes

CodeMeaning
not-a-foreach-loopthe caret is not in a foreach loop
not-indexablethe collection has no int indexer with a Length or Count, or the loop is an await foreach
collection-not-simplethe collection is computed, and the loop's condition would compute it on every iteration
name-conflictthe given index name is already declared or used in the loop's scope

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#array

A foreach over an array becomes an index loop that reads each element into the loop variable

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public int Sum(int[] values)
44 {
55 var total = 0;
6− /*^*/foreach (int value in values)
6+ for (int i = 0; i < values.Length; i++)
77 {
8+ int value = values[i];
89 total += value;
910 }
1011
1112 return total;
1213 }
1314 }

#collection-not-simple-rejected

Refuses a collection computed by a call, which the loop would call again on every iteration

refusal
TargetSample.cs, the /*^*/ caret
Refusescollection-not-simple; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 private int[] Values() => new[] { 1, 2, 3 };
6
7 public void Print()
8 {
9 /*^*/foreach (var value in Values())
10 {
11 Console.WriteLine(value);
12 }
13 }
14 }

#custom-index-name

The index takes the given name

success
TargetSample.cs, the /*^*/ caret
Arguments
name"index"
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void Print(int[] values)
66 {
7− /*^*/foreach (var value in values)
7+ for (int index = 0; index < values.Length; index++)
88 {
9+ var value = values[index];
910 Console.WriteLine(value);
1011 }
1112 }
1213 }

#deconstruction

A deconstructing foreach deconstructs the element at the index

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 public class Sample
55 {
66 public void Print(List<(string Name, int Age)> people)
77 {
8− /*^*/foreach (var (name, age) in people)
8+ for (int i = 0; i < people.Count; i++)
99 {
10+ var (name, age) = people[i];
1011 Console.WriteLine(name + " " + age);
1112 }
1213 }
1314 }

#embedded-statement

A body without braces gets a block holding the element and the statement

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void Print(int[] values)
66 {
7− /*^*/foreach (var value in values)
7+ for (int i = 0; i < values.Length; i++)
8+ {
9+ var value = values[i];
810 Console.WriteLine(value);
11+ }
912 }
1013 }

#explicit-conversion

A loop variable whose type needs an explicit conversion from the element gets a cast, as foreach converted it

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void PrintWhole(double[] amounts)
66 {
7− /*^*/foreach (int whole in amounts)
7+ for (int i = 0; i < amounts.Length; i++)
88 {
9+ int whole = (int)amounts[i];
910 Console.WriteLine(whole);
1011 }
1112 }
1213 }

#generic-list

A read-only list of a type parameter is indexed through the interface

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 public class Sample
55 {
66 public void PrintAll<T>(IReadOnlyList<T> entries)
77 {
8− /*^*/foreach (T entry in entries)
8+ for (int i = 0; i < entries.Count; i++)
99 {
10+ T entry = entries[i];
1011 Console.WriteLine(entry);
1112 }
1213 }
1314 }

#index-name-taken

When i is already declared in scope the index is named j

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void Print(int i, int[] values)
66 {
7− /*^*/foreach (var value in values)
7+ for (int j = 0; j < values.Length; j++)
88 {
9+ var value = values[j];
910 Console.WriteLine(i * value);
1011 }
1112 }
1213 }

#keeps-comments

Comments above the loop, after its header and in its body stay where they were

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void Print(string[] names)
66 {
77 Console.WriteLine("Names:");
88
99 // One line per name.
10− /*^*/foreach (string name in names) // every name
10+ for (int i = 0; i < names.Length; i++) // every name
1111 {
12+ string name = names[i];
1213 // Indented under the heading.
1314 Console.WriteLine(" " + name);
1415 }
1516 }
1617 }

#list-with-var

A foreach over a list counts up to its Count and keeps var for the element

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 public class Sample
55 {
66 public void Print(List<string> names)
77 {
8− foreach (var name /*^*/in names)
8+ for (int i = 0; i < names.Count; i++)
99 {
10+ var name = names[i];
1011 Console.WriteLine(name);
1112 }
1213 }
1314 }

#name-conflict-rejected

Refuses an index name already declared in the loop's scope

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"total"
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Sum(int[] values)
4 {
5 var total = 0;
6 /*^*/foreach (var value in values)
7 {
8 total += value;
9 }
10
11 return total;
12 }
13 }

#not-a-foreach-loop-rejected

Refuses a caret that is not in a foreach loop

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-foreach-loop; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Sum(int[] values)
4 {
5 var total = 0;
6 /*^*/for (int i = 0; i < values.Length; i++)
7 {
8 total += values[i];
9 }
10
11 return total;
12 }
13 }

#not-indexable-rejected

Refuses a sequence that cannot be indexed

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-indexable; every file is left unchanged
Sample.csinput
1 using System;
2 using System.Collections.Generic;
3
4 public class Sample
5 {
6 public void Print(IEnumerable<int> values)
7 {
8 /*^*/foreach (var value in values)
9 {
10 Console.WriteLine(value);
11 }
12 }
13 }

#nullable-context

In a nullable context the element keeps its nullable annotation

success
TargetSample.cs, the /*^*/ caret
Projectnullable enable
Sample.csmodified
11 using System.Collections.Generic;
22
33 public class Sample
44 {
55 public int CountMissing(IList<string?> labels)
66 {
77 var missing = 0;
8− /*^*/foreach (string? label in labels)
8+ for (int i = 0; i < labels.Count; i++)
99 {
10+ string? label = labels[i];
1011 if (label is null)
1112 {
1213 missing++;
1314 }
1415 }
1516
1617 return missing;
1718 }
1819 }

#string

A foreach over a string's characters counts up to its Length

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public int CountDigits(string text)
44 {
55 var digits = 0;
6− /*^*/foreach (char c in text)
6+ for (int i = 0; i < text.Length; i++)
77 {
8+ char c = text[i];
89 if (char.IsDigit(c))
910 {
1011 digits++;
1112 }
1213 }
1314
1415 return digits;
1516 }
1617 }