Шаблоны списков в 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<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;
Если вы используете более сложный синтаксис шаблона, то компилятор развернет его в более сложное выражение. Целью данной заметки не является разбор всех примеров, вы можете посмотреть, во что разворачивает ваш код компилятор, с помощью 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];
}