Pattern Matching in C#
Pattern matching is a relatively new feature in C#. It was first introduced to the C# language in C# 7.0 and has since seen additional improvements in each succeeding version of C#. This blog post will explore the various use cases for pattern matching as well as how pattern matching has evolved over each version of C#.
What is pattern matching?
So what is pattern matching? Pattern matching is the process of taking an expression and testing whether it matches certain criteria, such as ‘being a specified type’ or ‘matching a specified constant value’. C# supports various different patterns which will be explored within this blog post.
Common to all patterns is the Boolean match/no-match characteristic, so, the specified expression either matches the pattern or it does not. Pattern matching is a check and so can be used when branching code. The is
expression, switch
statement and the switch
expression (introduced in C# 8.0) all support pattern matching.
Why is pattern matching useful?
Pattern matching provides a more concise way of testing expressions. The different operations pattern matching allows can be done using traditional approaches. However, pattern matching can simplify these operations from lengthy if-else
statements into a more compact and readable block of code.
What are the different patterns?
There are a number of different ways to define a pattern which reflect the various operations pattern matching supports.
These are the classes which we will be using in the examples in this blog post:
public class Rectangle
{
public double Length { get; init; }
public double Height {get; init; }
}
public class Triangle
{
public double Base { get; init; }
public double Height { get; init; }
}
public class Circle
{
public double Radius { get; init; }
}
public class Square
{
public double Length { get; init; }
}
Patterns introduced in C# 7.0
Constant pattern
The constant pattern can be used to test whether an expression is equal to a specified constant. A popular use-case for the constant pattern is null checking. This checks whether an object is null
. The method in the following example uses the constant pattern to determine whether a Rectangle
object is null
.
Rectangle rectangle = new Rectangle { Height = 7, Length = 3 };
public void IsShapeNull(Rectangle rectangle)
{
if (rectangle is null)
{
throw new ArgumentNullException(nameof(rectangle));
}
}
Declaration pattern
The declaration pattern can be used to determine at run-time whether an expression is of a certain type. A variable declaration can optionally be included in the declaration pattern. If the test expression matches the specified type, then the expression will be cast to this type and then assigned to the variable.
The method in the following example tests whether shape
is of type Square
. If shape
is a Square
it will be downcast from object
to Square
and then will be assigned to a variable named square
. One expression contains the combination of both type checking and the cast. The else
block of the if-else
statement will be executed if shape
is not of type Square
. If there is not a type match, the square
variable is not created.
object shape = new Square { Length = 5 };
public void IsShapeSquare(object shape)
{
if (shape is Square square)
{
Console.WriteLine($"Shape is a square with a side of length {square.Length}");
}
else
{
Console.WriteLine($"{shape} is not a square");
}
}
Patterns introduced in C# 8.0
Property pattern
Pattern matching was further improved in C# 8.0 and one new addition was the property pattern. The property pattern can be used for checking and comparing values of properties. The property pattern tests whether an expression’s properties/fields match the values of specified properties/fields. Each corresponding property or field must match and the expression must not be null.
The example below shows a property pattern that inspects the Base
and Height
properties of a Triangle
object. The property pattern begins with checking whether the input is a specified type. Here, the property pattern adopts the same behaviour as the type pattern. Once the property pattern asserts the input is a Triangle
it will inspect the Base
property and the Height
property of the input. The pattern will match the expression if the input is a Triangle
with a Base
of 4 and a Height
of 6.
Triangle triangle = new Triangle { Base = 4, Height = 6 };
public void IsSpecificTriangle(Triangle triangle)
{
if (triangle is Triangle { Base: 4, Height: 6 } specificTriangle)
{
Console.WriteLine($"Shape is a triangle wih a base of {specificTriangle.Base} and a height of {specificTriangle.Height}");
}
}
var pattern
The var pattern can be used to match any expression and then assign it to a new declared variable. It is different to other patterns in that it always matches. So the purpose of a var pattern is to assign an expression to a variable, rather than testing an expression for a pattern. The var pattern is useful if you want to store property values in a variable if other patterns are matching, or if you want to store intermediate values in a temporary variable during calculations.
The method in the example below tests whether the input shape
is a Rectangle
and its Length
property is a multiple of 3. Here, we assign the value of the Length
property to a length
variable, then we test this variable in a Boolean expression.
object shape = new Rectangle { Length = 9, Height = 4};
public void IsLengthMultipleOfThree(object shape)
{
if (shape is Rectangle { Length: var length } rect && length % 3 == 0)
{
Console.WriteLine("This shape is a rectangle with a length which is a multiple of 3");
}
}
Positional pattern
The positional pattern is useful when testing a type that can be deconstructed. Deconstruction is a process of unpacking types into parts and storing them into new variables (object deconstruction). For example, a class can be split into its properties or a tuple can be split into its individual items.
The positional pattern can deconstruct an input expression and then test if the resulting variables match against a pattern specified in parentheses. C# offers built in support for deconstructing the record
and tuple
types. However, for other types, the compiler requires a Deconstruct
method to be implemented. For example, we can change the Rectangle
type to include a custom Deconstruct
method. Each value to be deconstructed is referred to by an out
parameter. The Deconstruct
method splits the Rectangle
type and returns a length
variable and a height
variable.
public struct Rectangle
{
public double Length { get; init; }
public double Height {get; init; }
public void Deconstruct(out double length, out double height)
{
length = Length;
height = Height;
}
}
Then, you can deconstruct an instance of the Rectangle
class named rectangle
with an assignment, such as follows:
Rectangle rectangle = new Rectangle { Length = 20, Height = 40 };
var (l, h) = rectangle;
In the example below, the rectangle
is deconstructed and length
is tested against the 20
pattern (a constant pattern) and height
is tested against _
(the discard pattern). The pattern reflects the position each variable has within the Deconstruct
method. So the first value coming out of the Deconstruct
method, is the first value we are going to match on and so forth. Discards (_
) can be used where any value is accepted at that position. So, the pattern will match if rectangle
has a length
of 20 and a height
of any value.
if (rectangle is (20, _) rect)
{
Console.WriteLine("The rectangle has a length of 20");
}
Tuple pattern
The tuple pattern can be used to pattern match multiple input values. The tuple pattern is a particular way of using the positional pattern, but the object we match on is not deconstructed as it is already a tuple.
The switch
expression (covered later in this blog post) in the example below uses the tuple pattern to select a shape description based on the values of the input tuple.
public string ReturnDescriptionOfShape(string shape, int length, int height)
{
return (shape, length, height) switch
{
("Rectangle", 2, 1) => "This is a small rectangle",
("Circle", 4, 2) => "This is a medium circle",
("Square", 8, 4) => "This is a large square",
(_,_,_) => "This not a valid input"
};
}
Patterns introduced in C# 9.0
Relational pattern
The relational pattern can be used for comparisons by testing how a value compares to a constant using comparison operators (>, <, >=, <=). The example below shows a relational pattern that inspects the Radius
property of a Circle
object. The pattern will match the expression if the input is a Circle
with a Radius
greater than or equal to 100.
object shape = new Circle {Radius = 110};
public void IsBigCircle(object shape)
{
if (shape is Circle { Radius: >= 100 })
{
Console.WriteLine($"This is a big circle");
}
}
Type pattern
Like the declaration pattern, you can use a type pattern to determine at run-time whether an expression is of a certain type. With the type pattern, a variable is not specified. The method in the following example tests whether shape
is of type Square
.
object shape = new Square { Length = 5 };
public void IsShapeSquare(object shape)
{
if (shape is Square)
{
Console.WriteLine($"{shape} is a square");
}
else
{
Console.WriteLine($"{shape} is not a square");
}
}
Logical patterns
C# 9.0 also introduced logical patterns, which is the ability to use and
, or
and not
pattern combinators to create the following logical patterns:
Negation or the not
pattern can be used for null checks. Before C# 9.0, the following code would be used to check if an object is not null
:
if (shape != null)
Checking if an object is not null
using the not
logical pattern in C# 9.0 can be done as follows:
if (shape is not null)
The syntax in the above example allows our code to express intent more clearly.
Conjuctive or the and
pattern can be used to check if the input expression matches more than one pattern. The method in the example below shows how you can combine two relational patterns to check that the Radius
property of a Circle
object is within a certain range. The pattern will match the expression if the input is a Circle
with a Radius
greater than or equal to 100 and less than or equal to 200. The expression must match both patterns for there to be a match.
object shape = new Circle {Radius = 110};
public void IsBigCircleRange(object shape)
{
if (shape is Circle { Radius: >= 100 and <= 200 })
{
Console.WriteLine($"This is a big circle");
}
}
Disjunctive or the or
pattern can be used to check if the input expression matches either one of the patterns specified. In the following example, the pattern will match the expression if the input is a Circle
with a Radius
that is equal to 100 or 200. Only one of these patterns needs to match the expression for there to be a match.
object shape = new Circle {Radius = 100};
public void IsBigCircleValue(object shape)
{
if (shape is Circle { Radius: 100 or 200 })
{
Console.WriteLine($"This is a big circle");
}
}
What are the different ways to do pattern matching?
The is
expression, switch
statement and the switch
expression (introduced in C# 8.0) all support pattern matching. The previous examples focused on how to pattern match with the is
expression, with the switch
expression being touched upon in the tuple pattern matching example. We will explore how the switch
statement and switch
expression can be used with the various different patterns in the next part of this blog post.
switch statement
The switch
statement has been empowered to support pattern matching. The switch
statement can be used when branching code by testing an expression against a set of patterns. As of C# 7.0, the switch
statement was improved to facilitate pattern matching. The switch
statement can now support any type, whereas only integral types and string constants were supported previously. Each case
is now an pattern and not a constant value. Finally, support of the when
keyword was included to further express and specify the condition for pattern matching.
The method in the example below calculates the area of a shape using the switch
statement to pattern match in order to select the correct formula for this calculation.
public double CalculateAreaSwitchStatement(T shape)
{
switch (shape)
{
case null:
throw new ArgumentNullException(nameof(shape));
case Square { Length: var l }:
return l * l;
case Circle { Radius: var r }:
return r * r * Math.PI;
case Rectangle { Length: var l, Height: var h }:
return l * h;
case Triangle { Base: var b, Height: var h}:
return b * h / 2;
default:
throw new NotSupportedException();
}
}
switch expression
C# 8.0 introduced the switch
expression to evaluate an expression against a set of patterns. The switch
expression provides a more concise syntax for pattern matching. There are several syntax improvements to the switch
expression as compared to the switch
statement. Firstly, the input variable is before the switch
keyword. The case
keyword and the colon (:) are replaced with arrows (=>) making the code more readable and concise. The default
case is now replaced with a discard (_
). Finally, the body is an expression, not a series of statements.
One of the big practical differences between a switch
expression and a switch
statement is that a switch
expression will tell you if you have not covered all possibilites. With a switch
statement, behaviour is specified for only some of the cases, with the default being to 'do nothing'. However, a switch
expression has to produce some sort of result when evaluated. This is useful as the compiler will tell you if you have forgotten something. For example, if you code a switch
for an enum
, and later a new value is added to the enum
, it is useful to be told when a switch
expression based on that enum is no longer complete as a result.
The method in the example below is the same as the previous example but using the switch
expression with the improvements described above. In this example, the discard pattern (_
) is used to match any expression, including null
. The discard pattern guarantees that the switch expression handles all possible input values. If the discard pattern is not used, and the input value does not match any of the patterns in the expression, then the runtime will throw an exception.
public double CalculateAreaSwitchExpression(T shape)
{
return shape switch
{
null => throw new ArgumentNullException(nameof(shape)),
Square { Length: var l } => l * l,
Circle { Radius: var r } => r * r * Math.PI,
Rectangle { Height: var h, Length: var l } => h * l,
Triangle { Base: var b, Height: var h } => b * h / 2,
_ => throw new NotSupportedException()
};
}
Conclusion
In this blog post we seen how pattern matching has evolved over each version of the C# language. We have explored the various different patterns that C# supports. We have also seen how the is
expression, switch
statement and the switch
expression all support pattern matching. Overall, pattern matching is a very powerful new feature in C# which can be utilised to make your code more concise and readable.