RefactorMCP catalog

Use Pattern Matching

Replaces a type test followed by casts, or an as conversion followed by a null check, with a declaration pattern in the if statement.

Precondition

The caret is on an if statement whose condition is one of:

  • A type test of a local or parameter, x is T, with casts (T)x in its branch. A field or property could change between the test and a cast, so only locals and parameters are converted, and x must not be assigned where the casts are.
  • A negated type test, !(x is T) or x is not T, whose branch always leaves (returns, throws, breaks or continues), with casts (T)x in the statements after the if.
  • A null check of a local declared by the statement just before the if and initialised with x as T:
  • t != null or t is not null, where t is only used in the condition and the branch, since the pattern variable is only assigned there;
  • t == null or t is null, whose branch always leaves and does not use t, so that the code after the if sees it assigned.

The pattern variable's name must not already be declared where it would be in scope.

Transformation

  • A type test becomes x is T name, or x is not T name when negated, and every cast (T)x where x is known to be a T becomes name, dropping the parentheses around it.
  • The name is the name argument when given; otherwise, when a local is initialised with nothing but the cast and never assigned, that local's name, and the declaration is removed; otherwise the type's name in camel case, numbered if taken.
  • An as conversion and its null check become x is T t or x is not T t, keeping the local's name unless name is given, and the declaration is removed. Comments on the declaration move to the if.

Preserved

  • Behaviour: the pattern tests the same type as the cast or conversion, and the variable holds the value the casts would have produced.
  • Uses of the variable in the rest of the method.

Limitations

  • Casts in an else branch, or in code outside the branch, are left as they are.
  • Type tests in switch statements and conditional expressions are not converted.
  • x as T where T is a nullable value type gives a pattern variable of the underlying type; uses such as .Value then no longer compile, and the change is refused.

Arguments

ArgumentMeaning
nameoptional name for the pattern variable

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
no-type-checkthe condition is neither a type test nor a null check of an as conversion just before the if
not-a-localthe tested value is not a local or parameter
no-castthere is no cast of the tested value to the tested type to replace
variable-assignedthe tested value is assigned where the casts are
branch-falls-throughthe test is negated, or checks for null, and its branch does not always leave
used-outside-ifthe local assigned with as is used where the pattern variable would not be assigned
name-conflictthe pattern variable's name is already declared

Cases

·

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

#as-then-null-check

An as conversion followed by a check that the result is not null becomes a declaration pattern

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 Shapes
99 {
1010 public double Radius(object shape)
1111 {
1212 // Only circles have a radius.
13− var circle = shape as Circle;
14− /*^*/if (circle != null)
13+ if (shape is Circle circle)
1514 {
1615 return circle.Radius;
1716 }
1817
1918 return 0;
2019 }
2120 }
2221 }

#as-then-null-guard

An as conversion followed by a null check that leaves becomes is not, keeping the variable for the code after it

success
TargetNames.cs, the /*^*/ caret
Projectnullable enable
Names.csmodified
11 namespace Shop
22 {
33 public class Names
44 {
55 public int Length(object value)
66 {
7− string? name = value as string;
8− /*^*/if (name is null)
7+ if (value is not string name)
98 {
109 return 0;
1110 }
1211
1312 return name.Length;
1413 }
1514 }
1615 }

#assigned-in-branch-rejected

Refuses a value assigned in the branch, after which a cast would see the new value

refusal
TargetSample.cs, the /*^*/ caret
Refusesvariable-assigned; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Length(object value, object other)
6 {
7 /*^*/if (value is string)
8 {
9 value = other;
10 return ((string)value).Length;
11 }
12
13 return 0;
14 }
15 }
16 }

#generic-type-parameter

A test against a type parameter becomes a declaration pattern of that type

success
TargetFinder.cs, the /*^*/ caret
Finder.csmodified
11 namespace Shop
22 {
33 public class Finder
44 {
55 public T Find<T>(object value) where T : class
66 {
7− /*^*/if (value is T)
7+ if (value is T t)
88 {
9− return (T)value;
9+ return t;
1010 }
1111
1212 return null;
1313 }
1414 }
1515 }

#is-then-cast-declaration

A type test followed by a local initialised with the cast becomes a declaration pattern with the local's name

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 Shapes
99 {
1010 public double Radius(object shape)
1111 {
12− /*^*/if (shape is Circle)
12+ if (shape is Circle circle)
1313 {
14− var circle = (Circle)shape;
1514 return circle.Radius;
1615 }
1716
1817 return 0;
1918 }
2019 }
2120 }

#is-then-casts

Every cast of the tested value in the branch is replaced by the new variable, named after the type

success
TargetBasket.cs, the /*^*/ caret
Basket.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Product
66 {
77 public string Name = "";
88 public decimal Price;
99 }
1010
1111 public class Basket
1212 {
1313 public decimal Total;
1414
1515 public void Add(object item)
1616 {
17− /*^*/if (item is Product)
17+ if (item is Product product)
1818 {
19− Console.WriteLine(((Product)item).Name);
20− Total += ((Product)item).Price;
19+ Console.WriteLine(product.Name);
20+ Total += product.Price;
2121 }
2222 }
2323 }
2424 }

#name-conflict-rejected

Refuses a name already in use where the pattern would declare it

refusal
TargetSample.cs, the /*^*/ caret
Arguments
name"other"
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Length(object value, object other)
6 {
7 /*^*/if (value is string)
8 {
9 return ((string)value).Length;
10 }
11
12 return 0;
13 }
14 }
15 }

#named-variable

The name argument names the new variable

success
TargetShapes.cs, the /*^*/ caret
Arguments
name"c"
Shapes.csmodified
11 namespace Shop
22 {
33 public class Circle
44 {
55 public double Radius;
66 }
77
88 public class Shapes
99 {
1010 public double Diameter(object shape)
1111 {
12− /*^*/if (shape is Circle)
12+ if (shape is Circle c)
1313 {
14− return ((Circle)shape).Radius * 2;
14+ return c.Radius * 2;
1515 }
1616
1717 return 0;
1818 }
1919 }
2020 }

#negated-branch-falls-through-rejected

Refuses a negated type test whose branch can run on into the code that casts

refusal
TargetSample.cs, the /*^*/ caret
Refusesbranch-falls-through; every file is left unchanged
Sample.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 public void Print(object value)
8 {
9 /*^*/if (!(value is string))
10 {
11 Console.WriteLine("not text");
12 }
13
14 Console.WriteLine(((string)value).Length);
15 }
16 }
17 }

#negated-guard

A negated type test whose branch always leaves becomes is not, and casts after it use the variable

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 Shapes
99 {
1010 public double Area(object shape)
1111 {
12− /*^*/if (!(shape is Circle))
12+ if (shape is not Circle circle)
1313 {
1414 return 0;
1515 }
1616
17− var radius = ((Circle)shape).Radius;
17+ var radius = circle.Radius;
1818 return 3.14 * radius * radius;
1919 }
2020 }
2121 }

#no-cast-rejected

Refuses a type test with no cast of the value to replace

refusal
TargetSample.cs, the /*^*/ caret
Refusesno-cast; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public bool IsText(object value)
6 {
7 /*^*/if (value is string)
8 {
9 return true;
10 }
11
12 return false;
13 }
14 }
15 }

#no-type-check-rejected

Refuses a condition that is neither a type test nor a null check of an as conversion

refusal
TargetSample.cs, the /*^*/ caret
Refusesno-type-check; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Twice(int value)
6 {
7 /*^*/if (value > 0)
8 {
9 return value * 2;
10 }
11
12 return 0;
13 }
14 }
15 }

#not-a-local-rejected

Refuses a test of a field, which could change between the test and the cast

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-local; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 private object _value = "";
6
7 public int Length()
8 {
9 /*^*/if (_value is string)
10 {
11 return ((string)_value).Length;
12 }
13
14 return 0;
15 }
16 }
17 }

#used-after-if-rejected

Refuses an as result used after the if, where the pattern variable would not be assigned

refusal
TargetSample.cs, the /*^*/ caret
Refusesused-outside-if; every file is left unchanged
Sample.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 public void Print(object value)
8 {
9 var text = value as string;
10 /*^*/if (text != null)
11 {
12 Console.WriteLine(text.Length);
13 }
14
15 Console.WriteLine(text);
16 }
17 }
18 }