Pattern Matching Constructs in C# 7

No comments

I have already covered local functions and tuples in C# 7. Today, I’ll play around with pattern matching. There’s actually two separate bits of syntax being called a single new feature. One extends the switch(){case} construct, the other extends is expressions. Like local functions and tuples, I think this adds some power for communicating the intention of the code. It can also be misused (what can’t?), a little more on that later. I hoped to do this article a week ago, but it took me longer to identify even a little additional insight over the MS docs than I expected. I’ve probably spent 15 hours fiddling around to come up with a somewhat meaningful sample!

As always with these “feature” articles, I am exploring the feature and that is it. The code examples should not be taken as design patterns or good practice. Where I see potential for good practice, I’ll call it out in the text.

When I hear “Pattern Matching”, the thing that comes to mind immediately is a regular expression. Microsoft’s definition of a regex on MSDN:

A regular expression is a pattern that the regular expression engine attempts to match in input text. A pattern consists of one or more character literals, operators, or constructs.

Pattern to me conjures a wide range of possible inputs that could be a match. When I look at the pattern matching in C#, the rules of what makes a match are far more strict than I’d personally call a “pattern”. I think the name they chose means we can expect more to be added to this feature in future releases.

What is it?

First, the simpler feature of extending is. Previously, if we wanted to see if a an object of type Employee was actually an instance of a subclass of Employee named SalesPerson and then call a method that only existed SalesPerson, we’d have to do something along these lines:

public decimal CalculateBonus(Employee employee)
{
    var salesPerson = employee as SalesPerson;
    if (salesPerson != null )
    {
        // .Sales and .Quota belong only to SalesPerson, not Employee.
        return (salesPerson.Sales - salesPerson.Quota) * 0.1m;
    }
    return employee.Salary * 0.1m;
}

The pattern matching on is simply allows us to combine those first two lines of the method. We now check for type SalesPerson and assign it to a variable in one line.

public decimal CalculateBonus(Employee employee)
{
    if (employee is SalesPerson salesPerson)
    {
        return (salesPerson.Sales - salesPerson.Quota) * 0.1m;
    }
    return employee.Salary * 0.1m;
}

If I need to check a property on salesPerson before running code, I can use normal logic with my new variable inside the same if expression. For example:

public decimal CalculateBonus(Employee employee)
{
    if (employee is SalesPerson salesPerson && salesPerson.IsCommissioned )
    {
        return (salesPerson.Sales - salesPerson.Quota) * 0.1m;
    }
    return employee.Salary * 0.1m;
}

There’s not much more that I can see to it, it’s just some welcome syntactic sugar that removes noisy code. I even used ILDASM to decompile both versions of the Release built code and didn’t see any difference in the output. The IL output was exactly the same, so I double checked to make sure of the time stamp on the file and then built it again to be sure. This appears to be purely about developer convenience and readability. Moving on…

More power if we switch gears

The switch(){case} improvement is more interesting. Now, we basically can combine if and switch together if we have some more complex rules we need to follow. Please be careful with this! I’ve learned over the years that switch gets hideously abused regularly. Huge swathes of business logic end up in insanely complex case statement blocks. It rarely starts out complex, it evolves over time as you add features and no one has the guts to refactor the mess. If you start finding yourself doing a lot of code like you’ll see me do with the SalesPerson, please refactor! Better off, just set some ground rules: one line of code, maybe two per case. If you need more than that, consult with the team and figure out what approach you should be using.

Anyhow, let’s be those bad coders and write our logic for calculating employee bonuses all in a single method, even though we know we’ll need to extend this to many other types of employees with lots of rules in the very, very near future.

public decimal Calculate(Employee employee, int fiscalYear)
        {
            if (employee == null)
                throw new ArgumentNullException(nameof(employee));
            if(employee.CurrentCompensation == null)
                throw new InvalidOperationException();
            if (!employee.CurrentCompensation.IsOvertimeExempt)
                return 0;
            var salesPerson = employee as SalesPerson;
            if (salesPerson !=null)
            {
                if (!salesPerson.SalesGoals.TryGetValue(fiscalYear, out SalesGoal salesGoal))
                    throw new KeyNotFoundException(
                        $"Unable to find FY{fiscalYear} sales goal for {employee.Number}:{employee.FamilyName}, {employee.GivenName}");
                var quotaX1Bonus = Math.Max(.1m * (salesGoal.FiscalYearSales - salesGoal.Quota), 0m);
                var quotaX2Bonus = Math.Max(.05m * (salesGoal.FiscalYearSales - 2 * salesGoal.Quota), 0m);
                return Math.Min(quotaX2Bonus + quotaX1Bonus, salesPerson.CurrentCompensation.Base * 3);
            }
            if (employee.Roles.Any(x => x == EmployeeRole.Management))
            {
                return employee.CurrentCompensation.Base * 0.1m;
            }
            if (employee.Roles.Any(x => x == EmployeeRole.Staff || x == EmployeeRole.Engineer))
            {
                return employee.CurrentCompensation.Base * 0.02m;
            }

            throw new ArgumentException(nameof(employee),
                $"Unable to calculate bonus for {employee.Number}:{employee.FamilyName}, {employee.GivenName}. The calculation strategy could not be determined.");

        }

What the new Pattern Matching in switch lets me do is mix in the same concept we had in is of matching a type and immediately assigning it to a variable. switch is no longer constrained to just be possible values of a variable, but you can actually use switch to distinguish subclasses without extra code that uses reflectoin or GetType(), etc. This is now possible:

switch (employee)
{
    case SalesPerson salesPerson:
        return (salesPerson.Sales - salesPerson.Quota) * 0.1m;
    case Executive executive:   
        // we magically got "companyProfit" from somewhere
        return companyProfit * executive.ProfitSharingPercentage;
    // more cases
    //...
    default:
        return employee.CurrentCompensation.Base * 0.1m;
}

But wait, there’s more! I can also use when to further filter my case. It’s not fancy at all, just use `case type variableName when expression. So, let’s add that rule about making sure the sales person is commissioned again:

switch (employee)
{
    case SalesPerson salesPerson when salesPerson.IsCommissioned:
        return (salesPerson.Sales - salesPerson.Quota) * 0.1m;    
    // more cases
    //...
}

If I want to check a property that exists on the parent type, I can. I have not found a way other than declaring a new variable inline with the case though. So, if I wanted to filter on a property of Employee, I would need my case to look something like the next chunk of code:

switch (employee)
{
    // ....
    case Employee emp when !emp.IsOvertimeExempt:
        return 0;
    // ...
}

If you’re using the early access preview of Resharper for 2017, it complains about Employee emp being redundant as employee is of type Employee, but I can’t see how to use the when without it.

Combining all of these features, I can now refactor my original code to use case to label each of my logical branches.

public decimal Calculate(Employee employee, int fiscalYear)
        {
            switch (employee)
            {
                case null:
                    throw new ArgumentNullException(nameof(employee));
                case Employee emp when emp.CurrentCompensation == null:
                    throw new InvalidOperationException();
                case Employee emp when !emp.CurrentCompensation.IsOvertimeExempt:
                    return 0;
                case SalesPerson salesPerson:
                    if (!salesPerson.SalesGoals.TryGetValue(fiscalYear, out SalesGoal salesGoal))
                        throw new KeyNotFoundException(
                            $"Unable to find FY{fiscalYear} sales goal for {employee.Number}:{employee.FamilyName}, {employee.GivenName}");
                    var quotaX1Bonus = Math.Max(.1m * (salesGoal.FiscalYearSales - salesGoal.Quota), 0m);
                    var quotaX2Bonus = Math.Max(.05m * (salesGoal.FiscalYearSales - 2 * salesGoal.Quota), 0m);
                    return Math.Min(quotaX2Bonus + quotaX1Bonus, salesPerson.CurrentCompensation.Base * 3);
                case Employee emp when emp.Roles.Any(x => x == EmployeeRole.Management):
                    return emp.CurrentCompensation.Base * 0.1m;
                case Employee emp1 when emp.Roles.Any(x => x == EmployeeRole.Engineer):
                case Employee emp2 when emp.Roles.Any(x => x == EmployeeRole.Staff):
                // note that I can't use emp1 or emp2 outside of the when
                // because I don't know which is actually assigned.
                // If I needed it, I'd have to separate the case labels 
                // or combine them into a single case.
                    return employee.CurrentCompensation.Base * 0.02m;
                default:
                    throw new ArgumentException(nameof(employee),
                        $"Unable to calculate bonus for {employee.Number}:{employee.FamilyName}, {employee.GivenName}. The correct calculation could not be determined.");
            }
        }

See what I did in the first case? It also allows us to treat null as a case too! Overall, I feel this is a bit more readable. It calls out each of my business cases and then shows me what is done. In reality, if I was going down this path, I’d probably separate those first two case labels out as they are validating bits aren’t null and not really business logic. Then I’d look at this as likely to run into lots of complex code for who is eligible for bonuses when and how that gets calculated. Then I would be looking to use a behavioral pattern such as Strategy to handle this.

Conclusion

The improvement to using is to assign is nice and convenient. I see no reason to not just start using it whereever you legitimately had the var x = y as MyClass; if(x != null){} pattern. Many times, you can refactor that out (such as with overloading), but it’s not always worth the complexity if you’re then doing something simple with the result of that assignment.

The switch enhancement is potentially powerful, but I strongly suggest that you be careful not to overuse it. I find switch at its most powerful when the code for each case is a single line. If it can’t be a single line (or just a few simple ones), look to create additional methods or look for another pattern. I could rant for hours about some of the bug factories I’ve seen created with switch gone wild, but it really just comes down to limiting the responsibilty of any given method. If you have to mentally break down the method into chunks and reason about each chunk separately in order to extend it, why not just break those chunks apart in code in some way too?

The new features in C# 7 that I have discussed so far can all be misused. That doesn’t mean we should avoid them, it means we should continue to be our usual responsible selves. Nothing we’ve ever gotten in C# or .Net in general has invalidated any good design pattern or practice, it’s just made them easier to implement. These new language enhancements fit that bill.fooisbar.png