DEV Community

Mark Clearwater
Mark Clearwater

Posted on • Originally published at blog.csmac.nz on

Looking Back on C# 7: Pattern matching

Looking Back on C# 7: Pattern matching

With C# 8 on our doorstep, I figure it is a good time to reflect on recent additions to the language that have come before. There are some great improvements you may have missed, some that I really enjoy using, and some I consider have reached canonical usage status that I think are all worth some reflection.

Pattern matching is a powerful feature that has been unlocked against various language constructs in C#. The idea is to take existing features like case from switches and is and extract their capabilities into a "pattern matching" concept over types and values. This can then be applied back to these language features, and other places in the future.

Before the new concept of patterns, case accepted a constant as an argument, and is would accept a type.

// Check the type of the variable "is assignable to" Shape
if(widget is Shape)
{
    switch ((widget as Shape).Type)
    {
        // When value matches the constant "Square"
        case "Square":
            // ...
            break;
        default:
            break;
    }
}

Enter fullscreen mode Exit fullscreen mode

With the introduction of C# 7.0, both of these language features have been enhanced to use the new Pattern Matching syntax. This is a backwards-compatible change, meaning that the pattern type can be a const or a type. As well as these existing cases, it also includes when clauses and var patterns as well.

is Expression

Originally, is was able to check a type. This could create more readable code but often left casting or as operator usage in the aftermath.

if (widget is Shape)
{
    var myShape = widget as Shape;
    // var myShape = (Shape)widget;
}

Enter fullscreen mode Exit fullscreen mode

This has been improved by the pattern matching. First, we have constant checking:

if (widget.Type is null)
{
    // ...
}

if (widget.Type is "FOO")
{
    // ...
}

Enter fullscreen mode Exit fullscreen mode

As well as this, the Type checking Pattern now includes support to create a scoped variable of the correct type, similar to the new out parameter functionality. This language feature is collectively known as "expression variables".

if (widget is Shape shape)
{
    // ... use `shape`
}

Enter fullscreen mode Exit fullscreen mode

Switch Statement

A switch used to only match on constants, but with the new pattern matching, we can do so much more.

void SwitchIt(object value)
{
    var result = 0;
    switch (value)
    {
        // We can still switch on constants, even when the types don't match
        case 0:
            break;
        // We can switch on type, and even create a scoped variable (like with `is`)
        case IEnumerable<int> childSequence:
        {
            foreach(var item in childSequence)
                result += (item > 0) ? item : 0;
            break;
        }
        // using `when`, we can do range or bounds checking
        case int n when n > 0:
            result += n;
            break;
        // We can constant check against null
        case null:
            throw new NullReferenceException("Null found in sequence");
        default:
            throw new InvalidOperationException("Unrecognized type");
    }

    return result;
}

Enter fullscreen mode Exit fullscreen mode

There is another new pattern that is useful with switch, which is the var pattern. The "var pattern" is similar to the type pattern, except that it always matches, but creates a variable with the assigned value.

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    case var s:
        // Always matches (similar to default) but gives access to the value as `s`
        WriteLine($"This is some kind of {s.Name} shape");
        break;
}

Enter fullscreen mode Exit fullscreen mode

It is worth pointing out that the order of your case arguments now matters. while not a logical breaking change, since it never mattered what order constants where declared, it would always match the right answer, this is a conceptual change you need to be aware of. Mixing and matching patterns in a switch mean that the order does matter, and the first pattern that matches will get executed. To repeat, when all cases are constants this wouldn't make any difference to the outcome but more advanced checks will.

In C# 7.1, the patterns were extended to work correctly with generic variables as well. In C# 7.0, you could use these type patterns as expected by first casting the T foo value to an Object (which could cause Boxing) and then the type checking would all work. In C# 7.1, this cast is no longer necessary, and also avoids any boxing and unboxing along the way. The docs go over this is more detail with an example comparing the two implementations, as there are a few subtle differences, especially around null/default cases.

Pattern Matching coming soon

This feature is an interesting one in the sense that it is both newer and probably under-used and less known. You can program away happily in C# never needing to use it and not coming across it.

But with C# 8, there is a bunch of new features that will be using this existing pattern matching coming, so you might want to get on board with this in preparation, because soon you will need to understand code using it, and probably see a lot more of it around.

Bring on the Switch Expression!

Top comments (0)