RefactorMCP catalog

Convert For to Foreach

Turns an index loop that only reads each element of a collection in turn into a foreach over the collection: for (int i = 0; i < items.Count; i++) with items[i] in the body becomes foreach (T item in items) with item. The reverse of Convert Foreach to For.

Target

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

Precondition

  • The loop declares one int index starting at 0, runs while it is less than the collection's Length or Count, and increments it by one with i++, ++i or i += 1.
  • The collection is a local, parameter, field or property, possibly through this, so evaluating it once gives the same collection.
  • The index is used in the body only to read collection[i], and not inside a lambda or local function, which would see the index change.
  • The body does not modify the collection: it does not assign it, assign its elements or members, or call a method on it that returns nothing or is known to change it (Remove, Pop, Dequeue and the Try forms).
  • The collection can be enumerated with foreach and yields the same elements it indexes.
  • The element's name is not already declared or used in the loop's scope.

Transformation

  • The loop becomes foreach (T name in collection) and each collection[i] in the body becomes name.
  • The element is declared with var when the index was, otherwise with the element's type, written as briefly as the scope allows. In a nullable context the type keeps its annotation.
  • The name is arguments.name, or the singular of the collection's name (orders gives order, entries gives entry, a leading underscore is dropped), or item when the collection's name is not a plural or its singular is taken.

Preserved

  • Behaviour: the same elements are read in the same order.
  • The body's statements, braces or their absence, and comments, including a comment after the loop's header.

Limitations

  • Loops that count down, start elsewhere, step by more than one or stop before the end are refused rather than converted with Skip, Take or Reverse.
  • Modifications through another reference to the same collection, or by a method that returns a value, are not detected.
  • A struct element whose members the body assigns is refused by the compile check rather than by a precondition, since a foreach variable is read-only.
  • The refactoring changes one loop, so there are no references in other files to update.

Error codes

CodeMeaning
not-a-for-loopthe caret is not in a for loop
unsupported-loop-shapethe loop does not count an index from 0 up to the collection's Length or Count by one
index-used-otherwisethe index is used other than to read the collection's element
collection-modifiedthe body changes the collection or its elements
not-enumerablethe collection cannot be enumerated with foreach to give the elements it indexes
name-conflictthe element's 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

An index loop that only reads the array's elements becomes a foreach over the array

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

#collection-changed-rejected

Refuses when the loop adds to the collection it walks

refusal
TargetSample.cs, the /*^*/ caret
Refusescollection-modified; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2
3 public class Sample
4 {
5 public void Repeat(List<int> values)
6 {
7 /*^*/for (int i = 0; i < values.Count; i++)
8 {
9 if (values[i] > 10)
10 {
11 values.Add(values[i] - 10);
12 }
13 }
14 }
15 }

#custom-name

The element variable takes the given name

success
TargetSample.cs, the /*^*/ caret
Arguments
name"price"
Sample.csmodified
11 public class Sample
22 {
33 private readonly decimal[] _data = new decimal[3];
44
55 public decimal Total()
66 {
77 decimal total = 0;
8− /*^*/for (int i = 0; i < _data.Length; i++)
9− total += _data[i];
8+ foreach (decimal price in _data)
9+ total += price;
1010 return total;
1111 }
1212 }

#element-written-rejected

Refuses when the loop assigns to the collection's elements

refusal
TargetSample.cs, the /*^*/ caret
Refusescollection-modified; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public void Double(int[] values)
4 {
5 /*^*/for (int i = 0; i < values.Length; i++)
6 {
7 values[i] = values[i] * 2;
8 }
9 }
10 }

#generic-list

A loop over a read-only list of a type parameter declares the element with that type

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− /*^*/for (int i = 0; i < entries.Count; i++)
8+ foreach (T entry in entries)
99 {
10− Console.WriteLine(entries[i]);
10+ Console.WriteLine(entry);
1111 }
1212 }
1313 }

#index-used-otherwise-rejected

Refuses when the index is used for something other than reading the element

refusal
TargetSample.cs, the /*^*/ caret
Refusesindex-used-otherwise; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 public void Print(string[] names)
6 {
7 /*^*/for (int i = 0; i < names.Length; i++)
8 {
9 Console.WriteLine(i + ": " + names[i]);
10 }
11 }
12 }

#keeps-comments

Comments above the loop 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− /*^*/for (int i = 0; i < names.Length; i++) // every name
10+ foreach (string name in names) // every name
1111 {
1212 // Indented under the heading.
13− Console.WriteLine(" " + names[i]);
13+ Console.WriteLine(" " + name);
1414 }
1515 }
1616 }

#list-with-var

A loop over a list's Count with a var index declares the element with var

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 public class Order
55 {
66 public decimal Amount { get; set; }
77 }
88
99 public class Sample
1010 {
1111 public void Print(List<Order> orders)
1212 {
13− for (var i = 0; /*^*/i < orders.Count; ++i)
13+ foreach (var order in orders)
1414 {
15− Console.WriteLine(orders[i].Amount);
16− Console.WriteLine(orders[i]);
15+ Console.WriteLine(order.Amount);
16+ Console.WriteLine(order);
1717 }
1818 }
1919 }

#name-conflict-rejected

Refuses an element 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 /*^*/for (int i = 0; i < values.Length; i++)
7 {
8 total += values[i];
9 }
10
11 return total;
12 }
13 }

#not-a-for-loop-rejected

Refuses a caret that is not in a for loop

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-for-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 var i = 0;
7 /*^*/while (i < values.Length)
8 {
9 total += values[i++];
10 }
11
12 return total;
13 }
14 }

#not-enumerable-rejected

Refuses a collection that has an indexer but cannot be enumerated

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-enumerable; every file is left unchanged
Sample.csinput
1 public class Row
2 {
3 private readonly int[] _cells = new int[4];
4
5 public int Count => _cells.Length;
6
7 public int this[int index] => _cells[index];
8 }
9
10 public class Sample
11 {
12 public int Sum(Row row)
13 {
14 var total = 0;
15 /*^*/for (int i = 0; i < row.Count; i++)
16 {
17 total += row[i];
18 }
19
20 return total;
21 }
22 }

#nullable-context

In a nullable context the element keeps the list's 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(List<string?> labels)
66 {
77 var missing = 0;
8− /*^*/for (int i = 0; i < labels.Count; i++)
8+ foreach (string? label in labels)
99 {
10− if (labels[i] is null)
10+ if (label is null)
1111 {
1212 missing++;
1313 }
1414 }
1515
1616 return missing;
1717 }
1818 }

#unsupported-loop-shape-rejected

Refuses a loop that does not start at the first element

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