C# 11 List Patterns - Create compatible types
C# 11 introduced list patterns. List patterns extend pattern matching to match sequences of elements in a list or an array. The goal of this post is not to explain the syntax but to show how to create a compatible type. If you don't know what list patterns are, I recommend reading the following documentation which contains many examples: List patterns documentation. Here're some examples:
var array = new[] { 1, 2, 3 };
_ = array is [1, 2, 3]; // Match a collection with 3 elements with the values 1, 2 and 3
_ = array is [1, _, 3]; // Match a collection with 3 elements with the values 1, any value and 3
_ = array is [var head, .. var tail]; // Match a collection with at least 1 element (head).
// tail contains the remaining elements ([2, 3])
List patterns work great with arrays or List<T>
. But what if you want to use it with your type? In this post, I describe how to create a .NET type that is compatible with the C# 11 list pattern syntax.
#How to create a type that is compatible with list patterns
When you use the list pattern syntax, the compiler will rewrite to expression to a simpler syntax. This is called lowering. The following example shows the lowering of the list pattern syntax:
var array = new[] { 1, 2, 3 };
_ = array is [1, 2, 3];
// Lowered by the compiler to:
_ = array != null && array.Length == 3 && array[0] == 1 && array[1] == 2 && array[2] == 3;
If you use more complex patterns, the compiler will lower the expression to a more complex expression. This is not the goal of this post to show the lowering of all possible patterns. You can use SharpLab to quickly inspect the code generated by the compiler and see how the compiler rewrites the code: SharpLab
The C# compiler uses duck typing to determine if a type is compatible with the pattern. This means you don't need to implement any interfaces or attributes. The compiler will check if the type has the required members and use them. Here's a list of the required members to support the list pattern syntax:
var collection = new MyCollection();
_ = collection is [var head, .. var tail];
public class MyCollection
{
// Gets the number of elements contained in the collection.
// Choose one of the following signatures.
// note: If both properties are present, the compiler will use Length
public int Length { get; }
public int Count { get; }
// Indexer, choose one of the following signatures
// The return type can be any type.
// note: If both indexers are present, the compiler will use this[Index index]
public object this[int index] => throw null;
public object this[System.Index index] => throw null;
// Choose one of the following signatures to support the slice pattern (..)
// The return type can be any type.
// note: If both methods are present, the compiler will use this[System.Range index]
public object this[System.Range index] => throw null;
public object Slice(int start, int length) => throw null;
}
Here's an example of a compatible type:
var collection = new MyCollection();
collection.Add(1);
collection.Add(2);
collection.Add(3);
_ = collection is [var head, .. var tail];
public class MyCollection
{
private readonly List<int> _items = new();
public void Add(int item) => _items.Add(item);
public int Length => _items.Count;
public int this[Index index] => _items[index];
public ReadOnlySpan<int> this[System.Range range]
=> CollectionsMarshal.AsSpan(_items)[range];
}
Do you have a question or a suggestion about this post? Contact me!