Span

Последнее обновление: 19.11.2023

Тип Span представляет непрерывную область памяти. Цель данного типа - повысить производительность и эффективность использования памяти. 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

Основные методы 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

Структура 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.

Ограничения Span

Как структура, определенная с модификатором ref, Span имеет ряд ограничений: она не может быть присвоена переменной типа Object, dynamic или переменной типа интерфейса. Она не может быть полем в объекте ссылочного типа (а только внутри ref-структур). Она не может использоваться в пределах операций await или yield.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850