RefactorMCP catalog

Convert If Chain to Switch Statement

Turns an if / else if chain that compares one value with constants or patterns into a switch statement on that value.

Precondition

  • The caret is on the first if of the chain.
  • The chain has at least two cases, counting a final else.
  • Every condition tests the same value, written the same way, in one of these forms:
  • x == c or c == x, where c is a constant, including null and enum members;
  • x is T or x is pattern;
  • several of those joined by ||, none declaring a pattern variable;
  • one of those followed by && condition.
  • The tested value has no side effects: the chain evaluates it once per comparison, where a switch evaluates it once.
  • No branch contains a break that belongs to an enclosing loop: in a switch it would leave the switch instead.
  • A comparison does not use a user-defined == (other than string's), which a constant pattern would not call, and does not compare with NaN, which == never matches but a constant pattern does.

Transformation

  • Each if becomes a switch section, in order:
  • x == c becomes case c:;
  • x is T t becomes case T t:, and x is null becomes case null:;
  • a || b becomes one label for each alternative;
  • x is T t && condition becomes case T t when condition:.
  • The final else becomes default:.
  • Each section runs its branch's statements, followed by break; unless they always jump away already.
  • Without a final else, there is no default section, and an unmatched value runs on to the code after the switch, as it ran on after the chain.

Preserved

  • Behaviour: cases are tried in the order of the chain, and each branch still runs only when its condition is the first to hold.
  • Comments before the chain, and comments inside each branch.

Limitations

  • Chains of separate if statements, each ending in a return, are not collected; only else if chains are.

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
too-few-casesthe chain has fewer than two cases
different-valuesthe conditions do not all test the same value
unsupported-conditiona condition is not a constant comparison or a pattern test that a case label can express
side-effectsthe tested value has side effects
contains-breaka branch contains a break of an enclosing loop

Cases

·

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

#break-in-branch-rejected

Refuses a branch with a break, which would leave the switch instead of the loop

refusal
TargetSample.cs, the /*^*/ caret
Refusescontains-break; every file is left unchanged
Sample.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 public void Scan(int[] values)
8 {
9 foreach (var value in values)
10 {
11 /*^*/if (value == 0)
12 {
13 break;
14 }
15 else if (value == 1)
16 {
17 Console.WriteLine("one");
18 }
19 }
20 }
21 }
22 }

#comments-kept

Comments inside each branch stay with its statements

success
TargetStock.cs, the /*^*/ caret
Stock.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Stock
66 {
77 public void Report(string code)
88 {
99 // Codes come from the warehouse.
10− /*^*/if (code == "A")
10+ switch (code)
1111 {
12− // Plenty left.
13− Console.WriteLine("available");
14− }
15− else if (code == "B")
16− {
17− Console.WriteLine("low"); // reorder soon
12+ case "A":
13+ // Plenty left.
14+ Console.WriteLine("available");
15+ break;
16+ case "B":
17+ Console.WriteLine("low"); // reorder soon
18+ break;
1819 }
1920 }
2021 }
2122 }

#constants

An else-if chain comparing one value to constants becomes a switch, with the final else as default

success
TargetShipping.cs, the /*^*/ caret
Shipping.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Shipping
66 {
77 public void Announce(int zone)
88 {
9− /*^*/if (zone == 1)
9+ switch (zone)
1010 {
11− Console.WriteLine("local");
12− }
13− else if (zone == 2 || zone == 3)
14− {
15− Console.WriteLine("national");
16− }
17− else
18− {
19− Console.WriteLine("international");
11+ case 1:
12+ Console.WriteLine("local");
13+ break;
14+ case 2:
15+ case 3:
16+ Console.WriteLine("national");
17+ break;
18+ default:
19+ Console.WriteLine("international");
20+ break;
2021 }
2122 }
2223 }
2324 }

#different-values-rejected

Refuses a chain that does not compare the same value throughout

refusal
TargetSample.cs, the /*^*/ caret
Refusesdifferent-values; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public string Describe(int a, int b)
6 {
7 /*^*/if (a == 1)
8 {
9 return "a";
10 }
11 else if (b == 2)
12 {
13 return "b";
14 }
15
16 return "neither";
17 }
18 }
19 }

#not-a-constant-rejected

Refuses a comparison with a value that is not constant

refusal
TargetSample.cs, the /*^*/ caret
Refusesunsupported-condition; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public string Describe(int value, int limit)
6 {
7 /*^*/if (value == 0)
8 {
9 return "zero";
10 }
11 else if (value == limit)
12 {
13 return "limit";
14 }
15
16 return "other";
17 }
18 }
19 }

#patterns

Type patterns and null checks become case labels, and && after a pattern becomes a when clause

success
TargetShapes.cs, the /*^*/ caret
Shapes.csmodified
11 namespace Shop
22 {
33 public class Circle
44 {
55 public double Radius;
66 }
77
88 public class Square
99 {
1010 public double Side;
1111 }
1212
1313 public class Shapes
1414 {
1515 public string Describe(object shape)
1616 {
17− /*^*/if (shape is null)
17+ switch (shape)
1818 {
19− return "nothing";
20− }
21− else if (shape is Circle circle && circle.Radius > 10)
22− {
23− return "big circle";
24− }
25− else if (shape is Circle)
26− {
27− return "circle";
28− }
29− else if (shape is Square square)
30− {
31− return "square of " + square.Side;
19+ case null:
20+ return "nothing";
21+ case Circle circle when circle.Radius > 10:
22+ return "big circle";
23+ case Circle:
24+ return "circle";
25+ case Square square:
26+ return "square of " + square.Side;
3227 }
3328
3429 return "unknown";
3530 }
3631 }
3732 }

#returning-branches

Branches that return need no break, and constants on either side of == are recognised

success
TargetGrade.cs, the /*^*/ caret
Grade.csmodified
11 namespace Shop
22 {
33 public enum Level
44 {
55 Low,
66 Medium,
77 High,
88 }
99
1010 public class Grade
1111 {
1212 public string Describe(Level level)
1313 {
14− /*^*/if (level == Level.Low)
15− return "low";
16− else if (Level.Medium == level)
17− return "medium";
18− else
19− return "high";
14+ switch (level)
15+ {
16+ case Level.Low:
17+ return "low";
18+ case Level.Medium:
19+ return "medium";
20+ default:
21+ return "high";
22+ }
2023 }
2124 }
2225 }

#side-effects-rejected

Refuses a compared value with side effects, which the chain evaluates once per comparison

refusal
TargetSample.cs, the /*^*/ caret
Refusesside-effects; every file is left unchanged
Sample.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 private int _next;
8
9 public string Take()
10 {
11 /*^*/if (Next() == 1)
12 {
13 return "one";
14 }
15 else if (Next() == 2)
16 {
17 return "two";
18 }
19
20 return "other";
21 }
22
23 private int Next() => _next++;
24 }
25 }

#single-condition-rejected

Refuses an if that is not part of a chain

refusal
TargetSample.cs, the /*^*/ caret
Refusestoo-few-cases; every file is left unchanged
Sample.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 public void Log(int value)
8 {
9 /*^*/if (value == 1)
10 {
11 Console.WriteLine("one");
12 }
13 }
14 }
15 }