Pattern matching in C# 9.0

15 minute read

Since its seventh version, C# has been borrowing more and more functional concepts from its little brother, F#.

Among these concepts, pattern matching is probably the one that will affect most how code is written.

This post wants to be a recap of all the patterns available up to C# 9.0 and how to use them.

What pattern matching is

Before delving into the several patterns supported by the C# language, let’s introduce the concept of pattern matching.

In this specific context, pattern matching is the act of checking whether a given object matches certain criteria.

These criteria can range from “being an instance of a type” to “having a property whose value is within a range of values”.

When to use pattern matching

Pattern matching excels at traversing complex object structures when the type system cannot help us.

A good example is when exploring an object received from an external REST API.

Also, it can be used to create a finite-state machine.

Where to use pattern matching

Pattern matching is a check, therefore it can be used wherever we are introducing a branch in our code.

Typical scenarios are:

  • if statements, with the help of the keyword is,
  • switch statements and expressions.

Test subjects

The classes below will be our test subjects for this post.

You will notice two things.

  • Even if they clearly represent shapes, these classes share no common ancestor but for object.
  • Their properties use the new keyword init. This means that we can set the value only via the constructor or when initializing the object.
public class Square
{
    public double Side { get; init; }
}

public class Circle
{
    public double Radius { get; init; }
}

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; }
}

Please notice that the lack of common ancestor is intended for the purpose of the example: pattern matching works well with class hierarchies as well.

Patterns

At this point, the language supports many kind of patterns: some are more common than others and all of them can be combined to create very powerful expressions.

Constant patterns

Constant patterns are the most basic one and therefore rarely used by themselves.

var rect = new Rectangle { Height = 6, Length = 4 };

if (rect.Height is 0)
{
    Debug.WriteLine("This rectangle has no height");
}

The major difference between using a constant pattern and using the equality operator (==) is that the first cannot be overridden unlike the latter one.

Constant patterns were introduced in C# 7.0.

Null patterns

A special case of the Constant pattern, a null pattern can be used to check whether an object is null.

if (rect is null)
{
    throw new ArgumentNullException(nameof(rect));
}

Similarly to constant patterns, null patterns are immune to any operator override, making their result more trustworthy.

Null patterns were introduced in C# 7.0.

Type patterns

A type pattern allows to quickly check if a variable is of a certain type and declare a variable of the checked type (this is also called capturing).

if (shape is Square sq)
{
    Debug.WriteLine($"Shape is a square with side equal to {sq.Side}");
}

This is equivalent to the following snippet.

if (shape is Square)
{
    Square sq = shape as Square;
    Debug.WriteLine($"Shape is a square with side equal to {sq.Side}");
}

Type patterns were introduced in C# 7.0.

Property patterns

Property patterns allow to explore the object’s properties and use other patterns to assert expectations on their value.

if (circle is { Radius: 0})
{

}

Property patterns can also be nested to fully explore a property hierarchy.

var person = new Person();

if (person is { Parent: { DateOfBirth: { Year: 1959 } } })
{

}

// Here is the definition of the Person class
public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime DateOfBirth { get; set; }

    public Person Parent { get; set; }
}

By itself, the property patterns might look redundant since you could achieve the same by chaining the properties,

if (person.Parent.DateOfBirth.Year == 1959) { }

We will discuss later how patterns can be combined to create powerful expressions that would require a lot of code to be replicated without pattern matching.

Property patterns were introduced in C# 8.0.

Relational patterns

The next step after combining property and constant patterns is comparing values.

Relational patterns help comparing value in terms of greater, less, greater or equal, less or equal.

In the snippet below, we are selecting circles whose radius is greater or equal than 100.

if (shape is Circle { Radius: >= 100 })
{
    // this is a huge circle
}

The notation used for the relational patterns is equivalent to the comparison operators.

  • > for greater than
  • >= for greater or equal than
  • < for less than
  • <= for less or equal than

Relational patterns are introduced in C# 9.0.

Negated patterns

If null patterns look very nice, the same can’t be said for their negated version.

if (!(shape is null))
{
    // shape is not null here
}

To remedy this issue, negated patterns are being introduced in C# 9.0.

This new kind of pattern allow to write null checks as simple and clear as the snippet below.

if (shape is not null)
{
    // shape is not null here
}

Truth be told, property patterns can be twisted to check for non-nullability.

if (shape is { })
{
    // shape is not null here
}

Negated patterns can be prepended to any other pattern (is not null is nothing more than a negated pattern in front of a null one).

if (shape is Circle { Radius: not 0 })
{
    // shape is a Circle with radius not equal to 0
}

In the snippet above we have combined four patterns with no effort and with a very elegant syntax:

  • type pattern
  • property pattern
  • negative pattern
  • constant pattern

It’s easy to see how powerful and expressive pattern matching can be.

Negated patterns are introduced in C# 9.0.

Conjunctive and Disjunctive patterns

Conjuntive and disjuntive patterns take their intimidating name from the logical operations known as conjunction and disjunction.

Very much like the boolean operators && and || are used to pair expressions, the and and or keywords are used to pair patterns.

Consider the following example:

if (shape is Circle { Radius: > 0 and < 100 } circle)
{
    // if shape is a Circle with a radius
    // between 0 and 100 (excluding boundaries),
    // we capture it into the `circle` variable
}

Here we are combining

  • a type pattern
  • a property pattern
  • a conjunctive pattern
  • two relational patterns, one for each side of the and keyword
  • two constant patterns, one for each side of the and keyword

In old C#, we could write the same snippet as follows:

if (shape is Circle)
{
    var circle = shape as Circle;

    if (circle.Radius > 0 && circle.Radius < 100)
    {
        // here we are!
    }
}

It is to be noted that and and or patterns are not perfectly equivalent: the list of patterns each can be paired to varies between and and or patterns.

  • and cannot be placed between two type patterns (unless they are targeting interfaces)
  • or can be placed between two type patterns but it doesn’t support capturing
  • and cannot be placed in a property pattern without a relational one
  • or can be placed in a property pattern without a relational one and supports capturing
  • or cannot be used between two properties of the same object
  • and cannot be used between two properties of the same object, but it is implicit

Here are some examples of patterns: not all of them are allowed by the compiler.

shape is Square and Circle // this will not compile
shape is Square or Circle // OK!
shape is Square or Circle smt // this will not compile
shape is Square { Side: 0 and 1 } // this will not compile
shape is Square { Side: 0 or 1 } sq // OK!
shape is Rectangle { Height: 0 or Length: 0 } // this will not compile
shape is Rectangle { Height: 0 } or Rectangle { Length: 0 } // OK!
shape is Rectangle { Height: 0 and Length: 0 } // this will not compile
shape is Rectangle { Height: 0, Length: 0 } re // OK! equivalent to the pattern above

It takes a bit of practice, but and and or can help save a lot of keystrokes!

Conjunctive and Disjunctive patterns are introduced in C# 9.0.

var patterns

While powerful and elegant, patterns can only be constant expressions.

var patterns help with many of the remaining cases by allowing us to declare a variable that is visible outside of the pattern being matched.

For example, let’s assure that the incoming shape is a Square whose side is a multiple of 2. Since patterns are constant expressions, we cannot perform the modulo calculation in the pattern.

if (shape is Square { Side: var side } sq && side % 2 == 0)
{
    // `sq` is a Square whose Side is a multiple of 2
}

In the snippet above, we work around the limitation of patterns by exporting a variable containing the value of the Side property and testing it in a normal boolean expression.

A var pattern can also be used to implement certain patterns in C# 8.0

For example, let’s rewrite a sample from the previous paragraphs using constructs available in C# 8.0

if (shape is Circle { Radius: var r } circle && r > 0 && r < 100)
{
    // here we are!
}

As usual, you have to imagine using these tools beyond the samples provided.

var patterns were introduced in C# 8.0.

Tuple patterns

Some algorithms depend on multiple inputs. Tuple patterns allow you to switch based on multiple values expressed as a tuple.

Here is a functional implementation of the typical FizzBuzz problem that uses Linq and the new switch expression (more information on this later) to process each number.

public void FizzBuzz(int limit)
{
    Enumerable.Range(1, limit)
              .Select(i => (i % 3 == 0, i % 5 == 0, i) switch
              {
                  (true, false, _) => "Fizz",
                  (false, true, _) => "Buzz",
                  (true, true, _) => "FizzBuzz",
                  (_, _, var n) => $"{n}"
              })
              .ToList()
              .ForEach(Console.WriteLine);
}

In the sample above, for each number the modulo of 3 and 5 is calculated and placed into a tuple of type (bool, bool, int).

We then proceed to evaluate each tuple matching it with a series of tuple patterns. The first pattern to match will decide the value to be returned.

To reduce the amount of cases, we can use the discard variable (i.e. _) to emphasize that we don’t have any expactation on that specific value.

The fourth case, (_, _, var n) uses a var pattern to carry the value of the tuple outside of the pattern for later usage.

Tuple patterns were introduced in C# 8.0.

Positional patterns

Positional patterns come handy when working with types that are decorated with a deconstructor.

In such cases, it is possible to define a pattern based on the values the object is deconstructed into.

For this example, we will amend the Rectangle class by adding a deconstructor to it.

public struct Rectangle
{
    public double Length { get; init; }
    public double Height { get; init; }

    public void Deconstruct(out double height, out double length)
    {
        height = Height;
        length = Length;
    }
}

Once we have added a deconstructor to a type (deconstructors can also be added via extension methods), we can use it to explode the object into different variables.

var rect = new Rectangle { Height = 10, Length = 5 };
var (h, l) = rect;
// h = 10, l = 5

The deconstructor can be used to create patterns somehow similar to the tuple ones.

if (rect is (10, _) re)
{
    // re.Height is 10
    // re.Length can be anything
}

In the snippet above, rect is deconstructed and its values are matched against the (10, _) pattern following the position they have in the deconstructor method (hence the positional name). Also, notice how we use the discard to express the fact that any value in the second position is accepted.

Positional patterns were introduced in C# 8.0.

New switch statement

In C# 7.0 the switch statement has been reworked and substantially empowered.

The changes are:

  • support for any type (before: only integral types such as int, string, byte, etc.)
  • cases are now expressions and not constant values anymore
  • case expressions are now evaluated in sequence, top to bottom
  • support for pattern matching
  • support of when keyword to further refine the case expression

Microsoft official documentation has a well-written article that explains all the new features of the switch statement introduced in C# 7.0.

Below is a method that calculates the area of a shape using different patterns to select the proper formula.

double CalculateAreaSwitchStatement(object obj)
{
    switch (obj)
    {
        case null:
            throw new ArgumentNullException(nameof(obj));

        case Square { Side: 0 }:
        case Circle { Radius: 0 }:
        case Rectangle re when re is { Length: 0 } or { Height: 0 }:
        case Triangle tr when tr is { Base: 0 } or { Height: 0 }:
            return 0;

        case Square { Side: var s }:
            return s * s;

        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();
    }
}

The method above is a bit of a show-off (there is no real need to check zero areas first) but it shows how the different patterns can be used to quickly explore different possibilities.

You can play with this example on .NET Fiddle.

At the time of writing, .NET Fiddle does not support C# 9.0 yet.

switch expression

In scenarios like the one above, where each case either returns a value or throws an exception, the statement behaves like an expression (see: Statement vs Expression).

To better handle these scenarios, C# 8.0 introduced the new switch expression.

Unlike for the statement version, which does not require a fallback case, the compiler requires (and enforces) that a valid path exists for every possible value of the control object of a switch expression.

Below is the same method to calculate the area rewritten using a switch expression instead of a statement.

You’ll notice that the syntax is very different. This is intended so that it’s easy to distinguish an expression from a statement.

double CalculateAreaSwitchExpression(object obj)
{
    return obj switch
    {
        null => throw new ArgumentNullException(nameof(obj)),

        Square { Side: 0 } => 0,
        Circle { Radius: 0 } => 0,
        Rectangle re when re is { Length: 0 } or { Height: 0 } => 0,
        Triangle tr when tr is { Base: 0 } or { Height: 0 } => 0,

        Square { Side: var s } => s * s,
        Circle { Radius: var r } => r * r * Math.PI,
        Rectangle { Height: var h, Length: var l } => l * h,
        Triangle { Base: var b, Height: var h } => b * h / 2,

        _ => throw new NotSupportedException()
    };
}

There is a good article on Microsoft official documentation’s website about switch expressions.

switch expressions are easy to nest. So, we can rewrite the method above to clearly split the cases for each different shape.

double CalculateAreaSwitchExpressionNested(object obj)
{
    return obj switch
    {
        null => throw new ArgumentNullException(nameof(obj)),

        Square sq => sq switch
        {
            { Side: 0 } => 0,
            { Side: var s } => s * s
        },

        Circle ci => ci switch
        {
            { Radius: 0 } => 0,
            { Radius: var r } => r * r * Math.PI
        },

        Rectangle re => re switch
        {
            { Length: 0 } or { Height: 0 } => 0,
            { Length: var l, Height: var h } => l * h
        },

        Triangle tr => tr switch
        {
            { Base: 0 } or { Height: 0 } => 0,
            { Base: var b, Height: var h} => b * h / 2
        },

        _ => throw new NotSupportedException()
    };
}

This version makes it easy to handle news shapes in the future.

Pattern matching vs classic evaluation

At first sight, the introduction of pattern matching might seem redundant. After all, many things can be done without it.

Let’s take a look at this simple pattern that is possible in C# 9.0

// C# 9.0
if (person is { Parent: { DateOfBirth: { Year: > 1970 and < 1990 }, LastName: "Smith" } })
{

}

Here, we are just checking that the parent of the person is born between 1970 and 1990 and has a certain last name. Nothing that couldn’t be achieved before.

// C# 7.0 and earlier
if (person.Parent.DateOfBirth.Year > 1970 && person.Parent.DateOfBirth.Year < 1990 && person.Parent.LastName == "Smith")
{

}

When comparing the two snippets, the first thing that can be noticed is how the newer version avoids replication of code.

Another important (but more subtle) difference is that since patterns are costant expressions, they are evaluated atomically. In the first snippet, the object either matches or not the pattern; in the second, the object has to go through three different checks.

In a multithread scenario this could be a problem if active countermeasures are not taken (e.g. using immutable objects).

The same issue applies when backporting the code to C# 8.0 (see snippet below).

Without the support for relational patterns, the year needs to be extracted using a var pattern and then evaluated with two different expressions and-ed with the pattern.

// C# 8.0
if (person is { Parent: { DateOfBirth: { Year: var year }, LastName: "Smith" } } && year > 1970 && year < 1990)
{

}

Generally, I tend to prefer using pattern matching whenever possible. It also fits really well my programming style based on short and pure functions.

Recap

In this post we have introduced pattern matching and explored the different patterns available in C# up to version 9.0 of the compiler. Also, we have seen how the switch statement has been empowered to support patterns and the new switch expression introduced in C# 8.0. Finally, we have looked at the differences between using newest patterns and the classic syntax.