Skip to content
Jessica Hill By Jessica Hill Apprentice Engineer III
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.

Jessica Hill

Apprentice Engineer III

Jessica Hill

Jessica comes from a Biosciences background, having gained a 1st Class Bachelor of Science in Biology from The University of Manchester.

During the lockdown Jessica used the opportunity to explore her interest in technology by taking two Code First Girls online courses; Introduction to Web Development and Python Programming.

This led Jessica to look for job opportunities in the technology sector and joined endjin's 2021 apprenticeship cohort, which had over 200 applicants.