Pattern Matching In C#

csharp

Because I’m programming a lot in Elixir, I am used to the concept of pattern matching. It has become so convenient, that I sometimes miss it in other languages. The more it makes me happy every time new pattern matching capabilities are coming to C#. Pattern matching is part of C# since version 7.0 and has evolved since then.

What is pattern matching?

Pattern matching means, that we can test that something fits onto something. What? Ok, let’s take a sheet of paper and write the number 2 on it. Now take a second sheet and write the number 1 on it. Cut the numbers out of the paper and stack the 1 onto the 2. If you look from above, does that match/fit? No. It does not perfectly overlap. Take a third sheet and write again the number 2 on it and cut it out. Stack this 2 onto the first 2. Does that match/fit? Yes. So this is basically, and in a very simplified way, how I imagine and describe pattern matching. I do have something, let’s say a string value, and I want to test it against some other string values. Again, like before, we take that value and stack it onto the first one of those other given values. If there is no difference, if it overlaps perfectly, we have a match.

string myStringValue = "something";
string message = myStringValue switch
{
  "nope"      => "This is not going to match",
  "something" => "But this will match",
  _           => "Not so much either"
};

Console.WriteLine(message)
//> But this will match

In the example above, the myStringValue first gets tested/matched against the value "nope". This clearly does not match. However, the second value, "something", does match. So the value that gets returned is "But this will match".

Syntax

But let’s first take a step back and talk about the syntax. In the preceding code I used the switch expression syntax. Maybe you are more familiar with the switch statement syntax, which, with the same example as above, looks like this:

string myStringValue = "something";

string message = string.Empty;
switch (myStringValue)
{
  case "nope":
    message = "This it not going to match";
    break;

  case "something":
    message = "But this will match";
    break;

  default:
    message = "Not so much either";
    break;
}

Console.WriteLine(message)
//> But this will match

Let’s look at the differences. First, I think it is save to say, that the switch expression is less code, more compact and easier to read. No case, break or default keywords needed. One big difference to keep in mind, is, that in switch expressions you cannot write multiple lines of code in the switch arm. The code after the => needs to be an expression. And the result of that expression gets returned, therefore no need for the return keyword. Last but not least, switch expressions have to return a value.

// NOT POSSIBLE

string myStringValue = "something";
string message = myStringValue switch
{
  "nope"      => {
    DoSomething();
    return "This is not going to match"
  },
  "something" => "But this will match",
  _           => "Not so much either"
};

// POSSIBLE

string myStringValue = "something";
string message = myStringValue switch
{
  "nope"      => NopeArmFunc(),
  "something" => "But this will match",
  _           => "Not so much either"
};

string NopeArmFunc()
{
  DoSomething();
  return "This is not going to match"
}

Console.WriteLine(message)
//> But this will match

Another way to match an expression against a pattern is by using the is operator. The is operator is for checking if a given expression is of a specific type. But since C# 7.0 it can also be used in pattern matching.

// Match against a constant pattern

string value = "test";
if (value is "test")
{
  Console.WriteLine("Content of 'value' is 'test'");
}

// Match against a property pattern (more on that later)

string value = "test";
if (value is { Length: > 3 })
{
  Console.WriteLine("The length of the value is larger than 3.");
}

Which patterns can be matched against in C#?

A lot. There is a whole list of patterns we can match against. We will explore some with examples in the next sections.

Constant patterns

In the examples above, I used string literals as patterns and those belong to the constant patterns. In this category we can also find integer, floating-point, char, boolean, enum and the null as possible patterns.

int grade = 2;
string message = grade switch
{
  1 => "very good",
  2 => "good",
  3 => "satisfying",
  4 => "enough",
  5 => "not enough",
  _ => throw new ArgumentException(nameof(grade))
};

Console.WriteLine(message);
//> good

bool result = true;
string value = MatchBoolean(result);
Console.WriteLine(value);
//> It's true

string MatchBoolean(bool arg)
  => arg switch
  {
    true  => "It's true",
    false => "It's false"
    // Here no 'default' arm is necessary, because `bool` can only be
    // `true` and `false`, and both cases are covered.
  };

Relational patterns

Also very handy are relational patterns. There it is possible to use the >, <, >= and <= operators as well as the and, or and not pattern combinators to match a specific range or values.

// An example with the `and` combinator

string msg = Temperature(18);
Console.WriteLine(msg);
//> warm

string Temperature(int temp)
  => temp switch
  {
    < -20           => "very cold",
    >= -20 and < 0  => "cold",
    > 0 and <= 10   => "a little cold",
    > 10 and <= 20  => "warm",
    > 20            => "hot",
    _ => throw new ArgumentOutOfRangeException(nameof(temp), $"{temp}")
  };

// An example with the `or` combinator

int value = GetCharScrabbleValue('a');
Console.WriteLine(value);
//> 1

int GetCharScrabbleValue(char c)
  => char.ToUpper(c) switch
  {
    'A' or 'B' or 'C' => 1,
    'D'               => 2,
    'E' or 'F'        => 3,
    _                 => 4
  };

Type patterns

We can also match against types.

Cat cat = new();
string value = MatchType(cat);
Console.WriteLine(value);
//> Hello Cat

string MatchType(Animal? animal)
  => animal switch
  {
    Cat  => "Hello Cat",
    Dog  => "Hello Dog",
    Bird => "Hello Bird",
    null => throw new ArgumentNullException(nameof(animal)),
    _ => throw new ArgumentException()
  };

interface Animal {}
record Cat : Animal;
record Dog : Animal;
record Bird : Animal;

Property patterns

With this type of pattern matching we can check/match against properties or fields of objects. Even nested property matching is possible. And again we can use relational operators.

string fullTitle = "Title: This is the title";
string title = GetTitle(fullTitle);
Console.WriteLine(title);
//> This is the title

string GetTitle(string value)
  => value switch
  {
    string { Length: >= 7 } s => s.Substring(7),
    _ => throw new ArgumentException("too short")
  };

// Object with nested properties
Address address = new("Streetname", "Vienna");
Person me = new("Me", address);
bool isFromVienna = IsFromVienna(me);
Console.WriteLine(isFromVienna ? "Yes, from Vienna", "No, not from Vienna");
//> Yes, from Vienna

bool IsFromVienna(Person p)
  => p switch
  {
    { Address.City: "Vienna" } => true,
    _ => false
  };

record Adress(string Street, string City);
record Person(string Name, Address Address);

A few things to mention regarding the preceding code examples. In the first example in the first switch expression arm, the input is matched against string { Length: >= 7 } s => .... Two things are happening here. First, the incoming string value is checked to be at least 7 characters long and second, is then assigned to a new local variable s. This is actually the var pattern, which is useful if you need to create a temporary var for future use. In that case it is not really needed, but I showed it here for demonstration purposes.

In the second example with the Person record, we can see how nested property matching works. I think important to mention is, that when matching an object against a pattern, not the whole object needs to match, only those parts of the pattern. In that case Address.City: "Vienna". Imaging the me object to look like that:

{
  Name: "Me",
  Address: {
    City: "Vienna,
    Street: "Streetname"
  }
}

Ok, that is a lot of pattern matching. But there is even more, which I have not covered here. For more details and more in-depth explanations I recommend you check out the documentation over at docs.microsoft.com. Or take a look at Part 2: Pattern Matching in C# - List Pattern