More Pattern Matching in C#

A while back I wrote a post C# 8.0 enhancements with pattern matching which was very light on the subject. Let’s look a little more in depth at options for pattern matching.

is/as in pattern matching syntax

Often (if you’ve been writing C# for a while) you’ll used to writing code like this

var person = o as Person;
if (person != null)
{
   Console.WriteLine(person.FirstName);
}

i.e. we have an object o which we don’t know the type of, so we use as to convert this to a Person type or null if it’s not of that type.

This can be replaced with a slightly more concise syntax (and what’s known as the declaration pattern).

if (o is Person person)
{
  Console.WriteLine(person.FirstName);
}

Null checks

This is a pattern known as a constant pattern

if (o is null)
{
  Console.WriteLine("Is null");
}

Along with a logical pattern using not we can also write

if (o is not null)
{
  Console.WriteLine("Is not null");
}

We can also use this pattern with types, for example

if (o is not (string or null))
{
  Console.WriteLine("Is NOT string or null");
}

<strong>Pattern matching when values match a criteria</strong>

If we extend the above Pattern Matching to not just check the type is a Person but also that the Age property is greater than 4, so we can now replace

[code language="csharp"]
if (o is Person p && p.Age > 4)
{
  Console.WriteLine($"Older than 4 {p.FirstName}");
}

with the following

if (o is Person { Age: > 4 } p)
{
   Console.WriteLine($"Older than 4 {p.FirstName}");
}

In the above we’re using a property pattern.

Switch patterns matching

Exhaustive switches can be used to match types using switch statements, for example

var result = o switch
{
  string s => $"String {s}",
  Person p => $"Person {p.FirstName}",
  _ => throw new ArgumentException("Unhandled type")
};

Note the use of the _ (discard) ensuring this is an exhaustive switch.

Better still we can also use other features of pattern matching in the switch like this

var result = o switch
{
  string s => $"String {s}",
  Person { Age: > 4} p => $"Person {p.FirstName} > 4",
  Person p => $"Person {p.FirstName}",
  null => "In Null",
  _ => throw new ArgumentException("Unhandled type")
};

In the above we’re switching based upon type and also matching values.

We can also use relational patterns, in this example we’ll assume o is an int

var result = o switch
{
  1 or 2 => "One or Two",
  > 2 and < 4 => "Mid",
  >= 4 and < 6 => "High",
  6 => "Six",
  _ => "Other"
};

Before we leave the switch statement we can also match against types using the “standard” switch syntax, i.e.

switch (o)
{
  case string s:
    Console.WriteLine(s);
    break;
  case Person p:
    Console.WriteLine(p.FirstName);
    break;
}

Tuples

Pattern matching also allows us to match against tuples and use the discard to ignore parts we’re not interested in, for example

var result = o switch
{
  (1, _) => "First One",
  (_, 0) => "Second Zero",
  _ => "Other"
};

Creating new variables from patterns

We can also use the var pattern to assign values from the patterns

var result = o switch
{
  var (a, b) when a < b => new Tuple<string, int, int>("First less than second", a, b),
  var (a, b) when a > b => new Tuple<string, int, int>("Second greater than first", a, b),
  var (a, b) => new Tuple<string, int, int>("Equals", a, b)
};

In this example we’re deconstructing a tuple and assigning to some new value (in this case an extended Tuple, just because I couldn’t think of a better example – check the documentation link above for a better example).