1. Введение
Вообразите, что у вас есть большой массив данных, например, считанные из файла байты. Вы хотите обработать его фрагмент — прочитать часть в виде строки или просто передать участок данных методу. В "старом" стиле вы бы копировали эти байты в новый массив — это медленно и тратит память. А если таких фрагментов десятки или сотни?
И вот тут появляется герой — Span<T>. Это особая структура, которая описывает срез (view) над массивом (или даже над любым непрерывным фрагментом памяти), не копируя данные, а просто указывая на нужную область. Важно: Span<T> — это не новая коллекция, а безопасное "окно" в существующий массив!
Основные особенности и ограничения Span<T>
- Span<T> — это структура (value type), представляющая "окно" в память и не копирующая элементы.
- Ссылается только на непрерывный участок памяти: массив, часть массива, stackalloc-блок, память строки через специальные методы, либо память в unsafe-коде.
- Span<T> живёт на стеке. Нельзя хранить его в поле класса или возвращать напрямую из async-методов.
- Гарантирует безопасность типов: доступ к памяти — без ручной работы с указателями (если вы не используете unsafe).
Кратко о новых возможностях C# 14
C# 14 расширяет работу со срезами: удобные диапазоны .. и индексы с конца ^1, улучшенные паттерны сопоставления для массивов и срезов и прочий "синтаксический сахар". К этому вернёмся в конце.
2. Как создать Span<T>?
Начнём с простого массива и создадим спан:
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // с 3-го элемента (индекс 2) взять 3 элемента
Теперь midSpan указывает на элементы 30, 40, 50 — это не копия, а взгляд на "живые" элементы массива. Изменяя их через спан, меняем исходный массив.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Выведет 100
Почему не использовать срезы массивов напрямую?
Классический LINQ-срез создаёт новый массив:
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- тут создается копия!
Это затратно по времени и памяти. Span решает проблему лишних копий — и работает не только с числами.
3. Ещё больше способов создать Span<T>
Существующий массив
Span<int> mySpan = numbers; // Неявное преобразование от массива к Span
Часть массива
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 элемента начиная с индекса 1: {20, 30, 40, 50}
Стековая память (stackalloc)
Span<T> позволяет выделять массивы на стеке:
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
Стековая память быстрая и освобождается автоматически при выходе из метода. Но объём должен быть разумным — мегабайты в стеке недопустимы.
Строки и ReadOnlySpan<char>
Строки в .NET неизменяемые, поэтому используем ReadOnlySpan<char>:
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Давайте соберём пример!
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Вся история заказов: ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("Вывести заказы со 2 по 4 (индексы 1-3):");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Мутируем данные через Span
recentOrders[1] = 999;
Console.WriteLine("После изменения через Span:");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Срез recentOrders реально работает напрямую с массивом — это не копия.
5. Безопасность, производительность и проверки
- Экономное обращение с памятью: никаких лишних копий.
- Проверки границ защищают от выхода за пределы массива.
- JIT оптимизации дают очень быстрый доступ (без unsafe).
Взаимодействие с методами
Методы могут принимать Span<T> или ReadOnlySpan<T>. Если менять данные не планируется — используйте ReadOnlySpan<T>.
static int Sum(Span<int> slice)
{
int sum = 0;
foreach (var item in slice)
sum += item;
return sum;
}
int[] data = { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(data.AsSpan(2, 3))); // 3+4+5=12
6. Использование диапазонов (range)
Начиная с C# 8, доступны диапазоны .. и индексы с конца ^ — они прекрасно работают со срезами.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // индексы 2,3,4 - то есть 30, 40, 50
В диапазоне начальный индекс включается, конечный — нет.
Индексы с конца
int lastElement = arr[^1]; // последний элемент (60)
Span<int> lastTwo = arr[^2..]; // два последних элемента (50, 60)
Диапазоны и строки
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Этот срез — ReadOnlySpan<char>; чтобы получить строку, вызовите ToString().
7. Современные паттерны работы со срезами (C# 14)
Новые паттерны упрощают разбор массивов и срезов.
if (arr is [10, 20, .. var rest]) // .. захватывает "хвост" массива
{
Console.WriteLine("Начало совпало, хвост:");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("Массив заканчивается на 50, 60");
Сравнение: Массив, ArraySegment и Span
| Тип | Мутируемый | Копирует данные? | Можно на стеке | Класс/структура | Можно делать поля класса |
|---|---|---|---|---|---|
|
да | - | нет | класс | да |
|
да | нет | нет | структура | да |
|
да | нет | да | структура | нет |
|
нет | нет | да | структура | нет |
Иными словами, Span<T> — это эволюция ArraySegment<T>: более производительно и безопасно.
8. Типичные ошибки и подводные камни
Попытка хранить Span<T> в поле класса. Нельзя: поля живут в куче, а Span<T> должен жить на стеке. Используйте ArraySegment<T> или индексы для адресации.
Возврат/хранение Span<T> из async-методов. Нельзя: асинхронность разрывает стек. Передавайте данные иначе — например, как массив, Memory<T>/ReadOnlyMemory<T>.
Неверный диапазон при создании среза. Выход за границы приводит к исключению во время выполнения. Всегда проверяйте длину и границы перед формированием среза.
Забыли, что спан отражает исходные данные. Любое изменение через Span<T> меняет исходный массив. Если нужна независимая копия — явно скопируйте данные.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ