Шаблоны списков в C#11

Шаблоны списков в C#11

В C#11 были добавлены шаблоны списков (List patterns). Шаблоны списков расширяют pattern matching для сопоставления последовательностей элементов в списке или массиве. Цель данной заметки не объяснить синтаксис, а разобраться, каким образом создать совместимый тип. Если вы не знакомы с pattern matching и с List patterns в частности, рекомендую прочитать статью в официальной документации Microsoft, где приведено достаточное количество примеров и исчерпывающей информации. Ниже представлены примеры List patterns:

var array = new[] { 1, 2, 3 };
_ = array is [1, 2, 3]; // Сопостовление коллекции содержащей 3 элемента со значениями 1,2,3.
_ = array is [1, _, 3]; // Сопостовление коллекции содержащей 3 элемента со значениями 1, любым числом, 3
_ = array is [var head, .. var tail]; // Сопастовление хотябы с 1 элементом (head).
                                      // хвост содержит элементы ([2, 3])
List patterns

List patterns прекрасно работает с массивами и списками объектов List<T>, но что, если вы захотите использовать шаблоны списков со своими типами? В данной заметке я расскажу вам как создать тип Net совместимый с синтаксисом list patterns C#11.

Как создать тип, который совместим с list patterns?

Когда вы используете синтаксис list patterns, компилятор разворачивает его в более простой синтаксис. Следующий пример показывает во что разворачивается list patterns синтаксис на более низком уровне:

var array = new[] { 1, 2, 3 };

_ = array is [1, 2, 3];
 // Разворачивается компилятором в:
_ = array != null && array.Length == 3 && array[0] == 1 && array[1] == 2 && array[2] == 3;
Во что разворачивается list patterns

Если вы используете более сложный синтаксис шаблона, то компилятор развернет его в более сложное выражение. Целью данной заметки не является разбор всех примеров, вы можете посмотреть, во что разворачивает ваш код компилятор, с помощью SharpLab.

Компилятор C# использует утиную типизацию, чтобы определить, совместим ли тип с шаблоном. Это означает, что вам не нужно реализовывать какие-либо интерфейсы или атрибуты. Компилятор проверит, есть ли у типа необходимые члены, и использует их. Вот список необходимых членов для поддержки синтаксиса list patterns:

var collection = new MyCollection();
_ = collection is [var head, .. var tail];

public class MyCollection
{
    // Получает количество элементов, содержащихся в коллекции.
    // Выберите одну из следующих сигнатур.
    // примечание: если присутствуют оба свойства, компилятор будет использовать Length
    public int Length { get; }
    public int Count { get; }

    // Индексатор, выберите одну из следующих сигнатур.
    // Возвращаемый тип может быть любым.
    // примечание: если присутствуют оба индексатора, компилятор будет использовать this[Index index]
    public object this[int index] => throw null;
    public object this[System.Index index] => throw null;

    // Выбирает одну из следующих сигнатур для поддержки slice pattern (...)
    // Тип возврата может быть любым.
    // Примечание: Если присутствуют оба метода, компилятор будет использовать this[System.Range index].
    public object this[System.Range index] => throw null;
    public object Slice(int start, int length) => throw null;
}

Вот пример совместимого типа:

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];
}