DEV Community

Dave Brock
Dave Brock

Posted on • Originally published at daveabrock.com on

C# 9 Deep Dive: Pattern Matching

In the previous post of this series, we discussed the power of records. That was a heavy topic.

For something completely different, we’ll discuss improved pattern matching in C# 9. This is not a completely new feature, but something that has evolved since it was first released way back in C# 7, albeit in basic form. This Microsoft article runs down the basics of pattern matching, which improved greatly in C# 8 as well. The pattern matching works with the is operator and with switch expressions, much of which I showed off in my article C# 8, A Year Late.

This is the third post in a five-post series on C# 9 features in-depth:

  • Post 1 - Init-only features
  • Post 2 - Records
  • Post 3 (this post) - Pattern matching
  • Post 4 (future post) - Target typing and covariant returns
  • Post 5 (future post) - Putting it all together: an all-in-one application

Heads up! C# 9 is still in preview mode, so much of this content might change—this post was last updated on July 6, 2020. I will do my best to update it as I come across it, but that is not guaranteed. Have fun, but your experience may vary.

First, get to know the C# 8 switch expression syntax

Before we get started with pattern matching enhancements in C# 9, much of it is based off the improved switch syntax from C# 8. (If you are already familiar, you can scroll to the next section.)

To be clear, they are now called switch expressions, and not switch statements. Before C# 8, you would typically have this (stolen from my C# 8 article):

public static string FindAProgrammingLanguage(string languageInput)
{
    string languagePhrase;

    switch (languageInput)
    {
        case "C#":
            languagePhrase = "C# is fun!";
            break;
        case "JavaScript":
            languagePhrase = "JavaScript is mostly fun!";
            break;
        default:
             throw new Exception("You code in something else I don't recognize.");
    };
    return languagePhrase;
}
Enter fullscreen mode Exit fullscreen mode

With switch statements, we can replace case and : with => and replace the default statement with _. That “underscore operator” is technically called a discard—a temporary, dummy variable that you want intentionally unused. This gives us a much cleaner, expression-like syntax.

Be honest: switch statements enable goto-like control flow (so we are clear on how I feel about this) and just executes code. I find the expressive style, which forces you to return a value, much better. You know that empty “well, better than a million if’s, I guess?” feeling you get with switch statements? This should make you feel better.

public static string FindAProgrammingLanguage(string languageInput)
{
    string languagePhrase = languageInput switch
    {
        "C#" => "C# is fun!",
        "JavaScript" => "JavaScript is mostly fun!",
         _ => throw new Exception("You code in something else I don't recognize."),
    };
    return languagePhrase;
}
Enter fullscreen mode Exit fullscreen mode

Now that we see how this improved C# 8 switch behavior helps you, let’s move on to pattern matching.

How pattern matching helps you

Pattern matching allows you to simplify scenarios where you need to cohesively manage data from different sources. An obvious example is when you call an external API where you don’t have any control over the types of data you are getting back. Of course, typically you would create types in your application for all the different types you could get back from this API. Then, you would build an object model off those types. This is a lot of work. What’s that old joke about object-oriented programming about the gorilla and the banana?

Imagine if you are working with multiple APIs! What if you provide shipping services, and are working with all the necessary APIs (FedEx, USPS, and more). You think they all got together to form one shared data model?

To make our lives easier, let’s sprinkle some functional, C# 9 magic on top of our OO language and make your life simpler.

(In-depth pattern matching techniques are beyond the scope of this post, but do check out Bill Wagner’s excellent work.)

If you want to play along, the easiest way as of now is to download LinqPad 6 Beta, then select Edit > Preferences > Query > Use Roslyn Daily build for experimental C# 9 support).

Our C# 8 baseline example

To build off our previous posts, let’s stick with the Iron Man theme. Here’s some C# 8 code we use to calculate a superhero’s fuel cost based on a maximum speed.

class Program
{
    static void Main(string[] args)
    {
        var superhero = new Superhero
        {
            FirstName = "Tony",
            LastName = "Stark",
            MaxSpeed = 10000
        };
        static decimal GetFuelCost(object hero) => 
        hero switch
        {
            Superhero s when s.MaxSpeed < 1000 => 10.00m,
            Superhero s when s.MaxSpeed <= 10000 => 7.00m,
            Superhero _ => 12.00m,
            _ => throw new ArgumentException("I do not know this one", nameof(hero))
        };
        Console.WriteLine(GetFuelCost(superhero)); // 7.00
    }
}

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string FavoriteColor { get; set; }
}

public class Superhero : Person
{
    public int MaxSpeed { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Relational patterns

With C# 9, we can simplify our switch expression using relational patterns. This allows us to use the relational operators such as <, <=, >, and >=. We can simplify our program—take a look at our new GetFuelCost method:

static decimal GetFuelCost(Superhero hero) => hero.MaxSpeed switch
{
    < 1000 => 10.00m,
    <= 10000 => 7.00m,
    _ => 12.00m
};
Enter fullscreen mode Exit fullscreen mode

Logical patterns

Similarly, you can use logical operators, like and, or, and not, as a complement to using relational patterns. This might be a more readable option for you if relational operators are not your jam.

Let’s try a slightly modified example with words instead of symbols:

static decimal GetFuelCost (Superhero hero) => hero.MaxSpeed switch
{
    1 or 2 => 1.00m,
    < 1000 and < 5000 => 10.00m,
    <= 10000 => 7.00m,
    _ => 12.00m
};
Enter fullscreen mode Exit fullscreen mode

You can also use the not operator, as I’ve highlighted in previous posts on C# 9 improvements.

As described in the Welcome to C# 9 post by Microsoft, it’s convenient if you use the null constant pattern:

not null => throw new ArgumentException($"Not a known person: {hero}", nameof(hero)),
null => throw new ArgumentNullException(nameof(hero))
Enter fullscreen mode Exit fullscreen mode

It also helps you think more clearly about negation logic. If you are used to something like this:

if (!(hero is Person)) { ...}
Enter fullscreen mode Exit fullscreen mode

Your co-workers will help you, if you change it to this:

if (hero is not Person) { ... }
Enter fullscreen mode Exit fullscreen mode

Wrapping up

In this post, we discussed the advantages of pattern matching, especially with coupled with powerful switch expressions, which were introduced in C# 8. We then discussed how C# 9 can help clean up your syntax with its relational and logical patterns.

Stay tuned for the next post, which discusses target typing and covariant returns in C# 9.

Top comments (0)