Тип Span представляет непрерывную область памяти. Цель данного типа - повысить производительность и эффективность использования памяти. Span позволяет избежать дополнительных выделений памяти при операции с наборами данных. Поскольку Span является структурой, то объект этого типа располагаетс в стеке, а не в хипе.
Для создания объекта Span можно применять один из его конструкторов:
Span()
: создает пустой объект Span
Span(T item)
: создает объект Span с одним элементом item
Span(T[] array)
: создает объект Span из массива array
Span(void* pointer, int length)
: создает объект Span, который получает length байт памяти, начиная с указателя pointer
Span(T[] array, int start, int length)
: создает объект Span, который получает из массива array length элементов, начиная с индекса start
Например, простейшее создание Span:
Span<string> people = ["Tom", "Bob", "Sam"];
В данном случае Span будет хранить ссылки на три строки.
Нередко Span создается на основе каких-то других наборов данных:
string[] people = { "Tom", "Alice", "Bob" }; Span<string> peopleSpan = new Span<string>(people);
Мы также можем непосредственно присвоить массив, и он неявно будет преобразован в Span:
string[] people = { "Tom", "Alice", "Bob" }; Span<string> peopleSpan = people;
Далее мы можем получать, устанавливать или перебирать данные также, как в массиве:
string[] people = { "Tom", "Alice", "Bob" }; Span<string> peopleSpan = people; peopleSpan[1] = "Ann"; // переустановка значения элемента Console.WriteLine(peopleSpan[2]); // получение элемента Console.WriteLine(peopleSpan.Length); // получение длины Span // перебор Span foreach (var s in peopleSpan) { Console.WriteLine(s); }
Если Span ведет себя внешне как массив, то в чем его преимущество или когда он нам может пригодиться? Рассмотрим простейшую ситуацию - у нас есть массив со значениями дневных температур воздуха за месяц, и нам надо получить их него два набора - набор температур за первую декаду и за последнюю декаду. Используя массивы, мы могли бы сделать так:
int[] temperatures = { 10, 12, 13, 14, 15, 11, 13, 15, 16, 17, 18, 16, 15, 16, 17, 14, 9, 8, 10, 11, 12, 14, 15, 15, 16, 15, 13, 12, 12, 11 }; int[] firstDecade = new int[10]; // выделяем память для первой декады int[] lastDecade = new int[10]; // выделяем память для второй декады Array.Copy(temperatures, 0, firstDecade, 0, 10); // копируем данные в первый массив Array.Copy(temperatures, 20, lastDecade, 0, 10); // копируем данные во второй массив
Для хранения данных создаются два дополнительных массива для дневных температур каждой декады. С помощью метода Array.Copy данные из исходного массива temperatures копируются в два остальных массива. Но суть в данном случае в том, что для обоих массивов мы вынуждены выделить память. То есть оба массива по сути содержат те же данные, что и temperatures, однако в отдельных частях памяти.
Span позволяет работать с памятью более эффективно и избежать ненужных выделений памяти. Так, используем вместо массивов Span:
int[] temperatures = { 10, 12, 13, 14, 15, 11, 13, 15, 16, 17, 18, 16, 15, 16, 17, 14, 9, 8, 10, 11, 12, 14, 15, 15, 16, 15, 13, 12, 12, 11 }; Span<int> temperaturesSpan = temperatures; Span<int> firstDecade = temperaturesSpan.Slice(0, 10); // нет выделения памяти под данные Span<int> lastDecade = temperaturesSpan.Slice(20, 10); // нет выделения памяти под данные
Для создания производных объектов Span применяется метод Slice, который из Spana выделяет часть и возвращает ее в виде другого объекта Span. Теперь объекты Span firstDecade и lastDecade работают с теми же данными, что и temperaturesSpan, а дополнительно память не выделяется. То есть во всех трех случаях мы работаем с тем же массивом temperatures. Мы даже можем в одном Span изменить данные, и данные изменятся в другом:
int[] temperatures = { 10, 12, 13, 14, 15, 11, 13, 15, 16, 17, 18, 16, 15, 16, 17, 14, 9, 8, 10, 11, 12, 14, 15, 15, 16, 15, 13, 12, 12, 11 }; Span<int> temperaturesSpan = temperatures; Span<int> firstDecade = temperaturesSpan.Slice(0, 10); temperaturesSpan[0] = 25; // меняем в temperatureSpan Console.WriteLine(firstDecade[0]); //25
За счет чего это достигается? Для понимания работы Span можно обратиться к исходному коду типа. В частности, мы можем в нем увидеть следующее свойство:
public readonly ref struct Span<T> { //.... public ref T this[int index] { get { ... } } //.... }
Здесь мы видим, что индексатор возвращает ref-ссылку, благодаря чем мы получаем доступ непосредственно к объекту и можем его изменять.
В данном случае, конечно, преимущества от отсутствия выделения дополнительной памяти под хранение объектов минимальны. Но при более интенсивной работе с данными выигрыш в производительности неизбежно должен возрастать.
Основные методы Span:
void Fill(T value)
: заполняет все элементы Span значением value
T[] ToArray()
: преобразует Span в массив
Span<T> Slice(int start, int length)
: выделяет из Span length элементов начиная с индекса start в виде другого Span
void Clear()
: очищает Span
void CopyTo(Span<T> destination)
: копирует элементы текущего Span в другой Span
bool TryCopyTo(Span<T> destination)
: копирует элементы текущего Span в другой Span, но при этом также возвращает значение bool,
которое указывает, удачно ли прошла операция копирования
Структура ReadOnlySpan аналогична Span, только предназначена для неизменяемых данных. Например:
string text = "hello, world"; string worldString = text.Substring(startIndex: 7, length: 5); // есть выделение памяти под символы ReadOnlySpan<char> worldSpan = text.AsSpan().Slice(start: 7, length: 5); // нет выделения памяти под символы //worldSpan[0] = 'a'; // Нельзя изменить Console.WriteLine(worldSpan[0]); // выводим первый символ // перебор символов foreach(var c in worldSpan) { Console.Write(c); }
В данном случае с помощью метода AsSpan() преобразуем строку в объект ReadOnlySpan<char> и затем выделяем из него диапазон символов "world". Поскольку ReadOnlySpan предназначен только для чтения, то соответственно мы не можем изменить через него данные, но получить можем. В остальном работа с ReadOnlySpan идет так же, как с Span.
Как структура, определенная с модификатором ref, Span имеет ряд ограничений: она не может быть присвоена переменной типа Object, dynamic или переменной типа интерфейса. Она не может быть полем в объекте ссылочного типа (а только внутри ref-структур). Она не может использоваться в пределах операций await или yield.