Pattern matching isn't new to programming, as it has been around for ages. And now it has been introduced in C#. To be clear, by "now" I mean in c#7 with a little pattern matching support and in c#8 with the real thing. Pattern matching is there to make our lives easier by changing the way we write certain condition statements.
If you want to download the code, it can be found at: https://github.com/Nivo1985/AdvancedCSharp. The project names match the blog post topic. In this case, since we are working on top of anonymous types, this is the project we will be working with.
The Plan
This article is divided into four demo files. In each of them we will look at one of the matching patterns. But the thing is, they work best together when we mix and match them to suit our needs. So, at some point it will be hard to tell the difference between them. Nevertheless, it is time to start coding.
The Utils
To demonstrate pattern matching, we need something to work with. Thetefore, I have created a set of four classes that we will use.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
namespace PatternMatching.ExampleClasses; public class Custorem { public int Id; public string Name; public Order Order; public Custorem(int id, string name, Order order) { Id = id; Name = name; Order = order; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
namespace PatternMatching.ExampleClasses; public class Order { public int Id; public string Name; public int NumberOfItems; public decimal Value; public Order(int id, string name, int numberOfItems, decimal value) { Id = id; Name = name; NumberOfItems = numberOfItems; Value = value; } public void Deconstruct(out string name, out int numberOfItems, out decimal value) { name = Name; numberOfItems = NumberOfItems; value = Value; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
namespace PatternMatching.ExampleClasses; public class RecurentOrder: Order { public Frequency Frequency; public RecurentOrder(int id, string name, int numberOfItems, decimal value , Frequency frequency) : base(id, name, numberOfItems, value) { Frequency = frequency; } } public enum Frequency { Daily, Weekly, Monthly, Yearly }
1 2 3 4 5 6 7 8 9 10 11 12 13 14
namespace PatternMatching.ExampleClasses; public class SpecialOrder: Order { public string ExtraRequests; public bool NextDayDelivery; public SpecialOrder(int id, string name, int numberOfItems, decimal value, string extraRequests, bool nextDayDelivery = false) : base(id, name, numberOfItems, value) { ExtraRequests = extraRequests; NextDayDelivery = nextDayDelivery; } }
The classes are quite simple, so I'll just take a quick look at them. At the very top we can see a customer class. This is just a starting point for building the object we need for a demo. Customer has one property that is of interest to us - type Order. The Order class can be seen in the second section of code, it has simple fields and is here to be used as a base class for two other classes.
The other two classes are RecurentOrder and SpecialOrder. They will serve as example elements for our discussion of pattern matching. The elements in them are self-explanatory.
Before we dive into the coding, I just want to make a disclaimer. Pattern matching hasn't brought some functionality that couldn't be achieved without it. They are "just" syntactic sugar to make our lives easier. With that out of the way, it is time to start coding.
Type Pattern
The first pattern matching technique I want to talk about is type pattern. This is the most intuitive and straightforward. To show how type patterns work, we have a demo class called DemosTypePattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
using PatternMatching.ExampleClasses; namespace PatternMatching.Demos; public class DemosTypePattern { public void Demo1() { var instance = new Custorem(1, "Cus1", new SpecialOrder(1, "Spec_Order_1",3, 30,"Make it spicy")); Console.WriteLine($"Test for {instance.Order.Name}"); Console.WriteLine(instance.Order is SpecialOrder ? "The order is special" : "The order not so special"); Console.WriteLine(instance.Order is Order ? "It is an order" : "It is not an order"); } public void Demo2() { var instance = new Custorem(1, "Cus1", new Order(2, "Order_2", 1 ,15)); Console.WriteLine($"Test for {instance.Order.Name}"); Console.WriteLine(instance.Order is SpecialOrder ? "The order is special" : "The order not so special"); Console.WriteLine(instance.Order is Order ? "It is an order" : "It is not an order"); } }
In the code above we can see the type of pattern technique where "is" is used. It allows us to determine whether an element is of a particular type. Previously, this effect was achieved using the GetType method and the type of keyword. However, this syntax is much more convenient.
In the code above you can see two example methods. In the first we have an object of the SpecialOrder class and in the second just an Order class. To see the results, we need to execute the code now.
1 2 3
var demosTypePattern = new DemosTypePattern(); demosTypePattern.Demo1(); demosTypePattern.Demo2();
The results of running the code are as follows. As we can see in Demo1, both conditions of the type pattern were true. Only the second condition was true in Demo2. Both results are as expected. This is a good start to understanding pattern matching.
Switch Expression
I know what you are going to say. "But this is not a pattern". I know, and you are absolutely right. But pattern matching and switch expressions work so well together that it would be a sin to separate them.
Switch expressions allow us to make checks an element again and return a value based on the result of the check. The pattern matching elements work excellently with this switch. Let's have a look at the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
using PatternMatching.ExampleClasses; namespace PatternMatching.Demos; public class DemoSwitchExpression { public void Demo1() { var instance = new Custorem(1, "Cus1" , new RecurentOrder(1, "Spec_Order_1",4,10, Frequency.Weekly)); var description = instance.Order switch { SpecialOrder { NextDayDelivery: true } => "Spacial with next day delivery", SpecialOrder => "Spacial", RecurentOrder => "Recurent", Order => "Plain", _ => "WTF" }; Console.WriteLine(description); } }
As we can see, we have a method that is supposed to determine the value of a description variable. It does this by putting a switch expression on the RecurentOrder class object. Inside the body of the switch expression there is a set of pattern-matching based conditions that determine the return value. SpecialOrder { NextDayDelivery: true }.
This one should be of particular interest to us, because it is a nested pattern match. First, we match for the type and when that is matched, we match for the property value. This is what makes pattern matching so flexible and useful.
If we set a breakpoint at the end and run the code, we will see something like this. This shows that our code worked correctly and that the object was a recurring one.
Positional Pattern
This is the most interesting of all the pattern matching techniques. The best way to discuss it is to show the code and build our discussion around it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
using PatternMatching.ExampleClasses; namespace PatternMatching.Demos; public class PositionalPattern { public void Demo1() { var instance = new Custorem(1, "Name1", new Order(1, "Karol Order", 110, 500)); var description = instance.Order switch { (var temp, >= 100, >=500) => $"The order {temp} is large and valuable", (_,>= 100,_) temp => $"The order {temp.Name} is large (ID: {temp.Id})", // values outside deconstructor (_,_, >=500) temp => $"The order {temp.Name} is valuable (ID: {temp.Id})", (_,_,_) temp => $"Nothing special about order {temp.Name} is valuable (ID: {temp.Id})" }; } public void Demo2() { var instance = new Custorem(1, "Name1", new Order(1, "Karol Order", 90, 500)); var instanceTuple = (instance.Order.Id, instance.Order.Name, instance.Order.NumberOfItems, instance.Order.Value); var description = instanceTuple switch { (_,var temp, >= 100, >=500) => $"The order {temp} is large and valuable", (_,_,>= 100,_) temp => $"The order {temp.Name} is large (ID: {temp.Id})", // values outside deconstructor (_,_,_, >=500) temp => $"The order {temp.Name} is valuable (ID: {temp.Id})", (_,_,_,_) temp => $"Nothing special about order {temp.Name} is valuable (ID: {temp.Id})" };
This example has two methods. We will start with the first one. In this method we have an Order object, and we test it in a switch expression using something like this (var temp, >= 100, >=500). This is a pattern-matching expression. It has three elements separated by a coma. An element can do one of two things, or both. It can set a condition or declare a variable.
In the given example we create a variable temp based on the value in the first position and we check the next two values. At this point you should ask a question about the source of the values. A very good point. If you look inside the code of the Order class, you will see a Destructor method.
1 2 3 4 5 6
public void Deconstruct(out string name, out int numberOfItems, out decimal value) { name = Name; numberOfItems = NumberOfItems; value = Value; }
This method provides us with the elements for positional pattern matching. The must have such an element if you want to use this technique on it.
The situation is different if you want to use tuple. In the case of these objects, it works out of the box - as you can see from the second method.
Property Pattern
This is where I have to be honest. This is the pattern I use most often, often in conjunction with the Type pattern. Mainly because it doesn't require any other work and is the most intuitive. The code will tell you why.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
using PatternMatching.ExampleClasses; namespace PatternMatching.Demos; public class DemosPropertyPattern { public void Demo1() { var instance = new Custorem(1, "Name1", new RecurentOrder(1, "Spec_Order_1",4,10, Frequency.Weekly)); var description = instance.Order switch { RecurentOrder {Frequency:Frequency.Daily} => "Recurent Daily", RecurentOrder {Frequency:Frequency.Weekly} => "Recurent Weekly", RecurentOrder {Frequency:Frequency.Monthly} => "Recurent Monthly", RecurentOrder {Frequency:Frequency.Yearly} => "Recurent Yearly", SpecialOrder {NextDayDelivery: true, Value: >100} => "Make it next day", Order => "Just a normal order" }; } public void Demo2() { var instance = new Custorem(1, "Name1", new SpecialOrder(1, "Spec_Order_1",3, 30,"Make it spicy",true)); var instanceTuple = (instance.Order.Id, instance.Order.Name, instance.Order.NumberOfItems, instance.Order.Value); var description = instance.Order switch { RecurentOrder {Frequency:Frequency.Daily} => "Recurent Daily", RecurentOrder {Frequency:Frequency.Weekly} => "Recurent Weekly", RecurentOrder {Frequency:Frequency.Monthly} => "Recurent Monthly", RecurentOrder {Frequency:Frequency.Yearly} => "Recurent Yearly", SpecialOrder {NextDayDelivery: true, Value: >100} => "Make it next day", not null => "Just a normal order" // null check }; } }
Here the pattern is placed SpecialOrder {NextDayDelivery: true, Value: >100}. This looks very similar to the position pattern, with one major difference. In this case we are referring directly to properties, which makes the code more readable compared to the position pattern, and it does require us to modify the class we want to use.
There is one element that distinguishes the two demo methods. I'm talking about the last condition in both switches. These are two approaches to the last condition, which should be as broad as possible. Please choose one and stick to it.
Summary
This was the seventh article in the Advanced C# series. I highly recommend that you start using pattern matching in your daily work. It will make your work faster and your code more readable.
If you have any questions, please drop me a line at karol.rogowski@softwarehut.com.
Until next time. Keep coding.