Additions to pattern matching in C# 10 and C# 11
As of today, since my first post about pattern matching in C#, Microsoft has released two new versions of the C# compiler.
For a quick recap about pattern matching in C#, check my previous post.
In regards to pattern matching, C# 10 introduced improvements on the property patterns and C# 11 introduced a whole new class of patterns, called list patterns. Also, C# 11 added the possibility to match patterns against Span<char>
and ReadOnlySpan<char>
as if they were proper instances of the type string
.
In this post, we’ll see what the new improvements are and how they can be used to write denser and, hopefully, more readable code.
For a full list of the new features introduced in the latest versions of the C# compilers, check these articles from the official Microsoft documentation:
Extended property patterns
In the previous post we’ve seen how a property pattern can help us detailing the shape of an object by its properties.
Let’s consider the same example I used in the previous post
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public Person Parent { get; set; }
}
If we wanted to check whether a person has a parent born in 1957, in C# 9 we would write
if (person is { Parent: { DateOfBirth: { Year: 1957 } } })
{
}
In C# 10, property patterns has been extended so that it’s possible to reference nested properties or fields.
The same code can be now written as follows
if (person is { Parent.DateOfBirth.Year: 1957 })
{
}
Obviously, using this new pattern is fully optional and it can be combined with the classic property pattern and all other patterns like in the snippet below.
if (person is { Parent: { LastName: "Golia", DateOfBirth.Year: 1957 } })
{
}
The advantage here is that the syntax is closer to what C# developers have been using for many years and it can be used to take away some visual noise from the many nested curly braces.
List patterns
If C# 10 introduced a minor improvement to an established pattern, C# 11 introduces a major feature that aims at facilitating working with sequences of items.
This set of patterns is generally referred to as list patterns and they can be used to match a list or an array against a sequence of patterns.
We can apply a list pattern to any type that is countable and indexable (i.e. it has an indexer that accepts an Index
as argument).
Before we start, let’s introduce our test types.
public class Square
{
public double Side { get; init; }
}
public class Circle
{
public double Radius { get; init; }
}
public class Rectangle
{
public double Length { get; init; }
public double Height { get; init; }
}
public class Triangle
{
public double Base { get; init; }
public double Height { get; init; }
}
And let’s define a test sequence that we can use in our samples.
var shapes = new object[]
{
new Rectangle { Height = 25, Length = 4 },
new Square { Side = 10 },
new Circle { Radius = 5 },
new Rectangle { Height = 4, Length = 25 },
new Square { Side = 7 },
new Circle { Radius = 5 },
new Triangle { Base = 25, Height = 8 }
};
Since the test types have no common ancestor other than object
, I used a object[]
for my collection sample. Alternatively, I could have used List<object>
or the old ArrayList
.
Let’s start by testing if our list has a first item of type Rectangle
and a second one of type Square
if (shapes is [Rectangle, Square, ..]) { }
From the snippet above, you can see that the pattern can be decomposed in a series of sub-patterns wrapped between two brackets.
You can also see that we use the ..
syntax to represent the rest of the sequence. This is also referred to as slice pattern and can be used to match any sequence that is countable and sliceable (i.e. it has an indexer that accepts a Range
as argument)
For brevity, from now on, I will only be writing the condition using the pattern match rather than the full if
statement.
The slicing pattern can be used anywhere in the sequence of sub patterns.
shapes is [.., Circle, Triangle]
In the snippet above, I’m using the ..
syntax to slide through the whole sequence and check if the last two items matche the given type pattern.
Since we can use any pattern, we can leverage the discard pattern and the var pattern to discard or pick a specific item of the sequence.
In the snippet below, I’ll be checking if the second last item is a Circle and save the value of its radius in a variable.
if (shapes is [.., Circle { Radius: var radius}, _])
{
Console.WriteLine(radius);
}
It’s interesting to see what the compiler generates for the snippet above. To do so, I used SharpLap and reinterpreted the output in something a bit more digestable.
if (shapes != null)
{
int num = shapes.Length;
if (num >= 2)
{
Circle circle = shapes[num - 2] as Circle;
if (circle != null)
{
var radius = circle.Radius;
Console.WriteLine(radius);
}
}
}
You can see the full snippet here
Unfortunatly, I wasn’t able to use pattern matching to find a sequence at a random spot within a sequence. Ideally, I’d write a template as in the snippet below, but the compiler supports only a single slice pattern at once.
// THIS CODE WON'T COMPILE
shapes is [.., Circle, Rectangle, ..]
I asked on Twitter and Reddit if people had a solution and u/afseraph suggested an interesting workaround.
if (DoesContainCircleRectangle(shapes))
{
Console.WriteLine("Yes");
}
static bool DoesContainCircleRectangle(Span<object> shapes)
{
return shapes switch
{
[] => false,
[Circle, Rectangle, ..] => true,
_ => DoesContainCircleRectangle(shapes[1..])
};
}
The snippet above uses recursion and a switch expression to scan through the sequence until the sequence we search is found. For those not used to the syntax, shapes[1..]
returns the items of the shapes
array from the second item onward. Props to u/afseraph for the creative solution.
Finally, you can read the official documentation about list patterns. The most daring can give a peek at the original proposal for the compiler feature.
Expanded targets for constant string
This is quite a smaller modification that will benefit mostly library authors that use Span<char>
and ReadOnlySpan<char>
to write high-performing and low-allocating code.
Library authors will now be able to write something like the following snippet
Span<char> chars = "hello world";
var isMatch = chars[0..5] is "hello";
Developers don’t generally explictly use Span<T>
and ReadOnlySpan<T>
but these two classes are behind most of the performance improvements of the latest versions.
Recap
In this post we’ve looked at the latest additions to pattern matching introduced in C# 10 and C# 11.
Support this blog
If you liked this article, consider supporting this blog by buying me a pizza!