C# 11 List Patterns - Create compatible types

 
 
  • Gérald Barré

C# 11 introduced list patterns, which extend pattern matching to match sequences of elements in a list or an array. This post focuses on how to create a type compatible with list patterns, not on explaining the syntax itself. If you are not familiar with list patterns, the following documentation provides many examples: List patterns documentation. Here are some examples:

C#
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 them with a custom type? The following section covers what members your type needs to expose to be compatible.

#How to create a type that is compatible with list patterns

When you use the list pattern syntax, the compiler rewrites the expression to a simpler form. This process is called lowering. The following example shows how the compiler lowers a list pattern:

C#
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 correspondingly complex form. Covering the lowering of all possible patterns is out of scope for this post. You can use SharpLab to quickly inspect the code generated by the compiler: SharpLab

The C# compiler uses duck typing to determine if a type is compatible with a list pattern. This means you don't need to implement any interfaces. The compiler checks whether the type exposes the required members and uses them accordingly. Here are the required members to support list pattern syntax:

C#
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 is an example of a compatible type:

C#
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!

Follow me:
Enjoy this blog?