1. Введение
Когда вы работаете с массивами, строками и байтовыми буферами, часто возникает задача “посмотреть” на часть этих данных. Например, выделить подстроку, взять срез массива, обработать часть входящего потока. В старых версиях .NET для этого приходилось либо копировать данные (создавать новый массив/подстроку), либо писать код, обходя массив с нужного индекса до нужного конца. Всё это не слишком приятно ни по производительности, ни по читаемости.
Вот пример старого подхода: нам нужно передать в метод только часть большого массива:
// Старый подход — копируем часть массива (неэффективно!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // создаётся новый массив
Таким образом, если нам нужно эффективно передавать “срез” массива (или даже куска строки), не создавая лишние объекты, старые средства C# явно проигрывают, особенно если у нас дело с большими объёмами информации.
Вот тут-то и приходит на помощь герой дня — Span<T>!
2. Что такое Span<T>? Основная идея
Span<T> — это тип, который представляет непрерывную область памяти одного и того же типа T. Его задача — дать вам быстрый, безопасный и эффективный способ работы с кусками массивов, строк, структур, а также с неуправляемой памятью (например, память, выделенная вне управляемой среды .NET).
Главная фишка Span<T> — это “срезы без создания новых массивов”. Представьте линейку, с помощью которой вы можете измерять любые участки одного и того же массива без копирования данных и с минимальным риском ошибиться в индексах.
Кратко:
- Span<T> — “окно” или “вид” на кусок памяти, которым можно удобно и безопасно манипулировать.
- Нет выделения новой памяти — экономия ресурсов и меньше работы для GC.
- Работает не только с массивами, но и с сегментами строк, stackalloc-блоками и даже с неуправляемой памятью.
- Нельзя хранить в полях обычного класса: это стековый тип (stack-only struct).
Почему это важно?
В высокопроизводительных задачах (парсинг файлов, обработка больших буферов, криптография, сериализация) экономия даже пары копирований массивов может дать огромный прирост скорости и снизить нагрузку на систему сборки мусора (GC). А ещё вы просто покажете коллегам, что следите за современным C# и .NET!
3. Базовое использование Span<T>: первый срез
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Создать Span на часть массива (например, элементы со 2 по 5 включительно)
Span<int> middle = new Span<int>(numbers, 2, 4); // индексы: 2, 3, 4, 5
// Мы видим подмассив: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6
// Изменение Span изменяет исходный массив!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999
Важно! Span<T> не копирует данные, а лишь указывает на “кусок” массива. Все изменения видны и в исходном массиве, и в Span.
4. Основные способы создания Span<T>
На основе массива:
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // полная длина
Span<int> slice = arr.AsSpan(1, 3); // элементы 20, 30, 40
На основе части массива:
Span<int> part = new Span<int>(arr, 2, 2); // элементы 30, 40
stackalloc: выделение памяти на стеке (крайне быстро и не попадает в “кучу”):
Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;
С помощью метода .Slice():
Span<int> subSpan = span.Slice(1, 2); // элементы 20, 30
Визуальная схема “среза”
Изначальный массив: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
<--- Span: 3, 4, 5, 6 --->
5. Ограничения и особенности Span<T>
- Stack-only! Нельзя хранить в “обычных” полях класса или делать частью замыкания — это стековый тип.
- Нельзя использовать как поле класса или возвращать из async-методов (компилятор выдаст ошибку).
- Нельзя захватывать в лямбдах/анонимных методах — используйте “здесь и сейчас”.
- Нельзя напрямую сериализовать или передавать между потоками.
Это связано с тем, что Span может указывать на любую область памяти, и если он вдруг “переедет” в кучу, можно получить небезопасные состояния.
6. Иммутабельность: ReadOnlySpan<T>
Иногда нужно “взглянуть” на часть памяти, но не изменять её. Для этой задачи есть неизменяемый вариант — ReadOnlySpan<T>.
string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n
// letters[0] = 'Z'; // Ошибка: индексатор только для чтения!
Классический сценарий — безопасная передача “куска” строки или массива куда-то, где его нельзя (и не хочется) менять.
7. Практика на примере: “Срезаем” массивы и строки
Допустим, это анализатор данных, который из большой строки выделяет подстроку, ищет в ней числа и возвращает их сумму (без лишних копирований при первичном срезе):
using System;
class Program
{
static void Main()
{
// Допустим, пользователь ввёл длинную строку чисел, разделённых пробелами
string input = "12 34 56 78 90 123 456 789";
// Нам нужно рассчитать сумму чисел только из "центра", например, 56 78 90
// Выделяем подстроку (но не копируем её!)
ReadOnlySpan<char> center = input.AsSpan(6, 8); // индексы можно вычислить динамически
// Парсим числа через Split (создаём временный массив)
string[] numbers = center.ToString().Split(' ');
int sum = 0;
foreach (var str in numbers)
{
if (int.TryParse(str, out int num))
sum += num;
}
Console.WriteLine($"Сумма центральных чисел: {sum}");
}
}
Современные библиотеки парсинга CSV и JSON используют Span для высокой скорости работы с большими объёмами строковых данных — теперь вы знаете, на чём основано их “волшебство”.
8. Полезные нюансы
Span против копирования массивов
// Старый способ: копируем кусок массива
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // создали новый массив на 1000 элементов
// Новый способ: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // не копируем вообще!
bestThousand[0] = 42; // меняется и в arr
Особенно разница заметна при интенсивных парсингах файлов, обработке сетевых буферов, работе с бинарными данными.
Применение в реальных задачах: зачем знать про Span
- Высокопроизводительный парсинг и обработка строковых/бинарных данных. Современные библиотеки сериализации (например, System.Text.Json, Span в документации Microsoft) используют Span для ускорения работы.
- Буферизация и чтение файлов (разрезаем большие буферы без копирования).
- Обработка данных с ограничением по памяти (embedded, IoT) — официальная документация по Memory/Spans.
- Алгоритмы работы с изображениями и аудио, где важна скорость и отсутствие лишних аллокаций.
- Ускорение парсинга CSV, JSON, XML с помощью Span — особенно в .NET 8/9.
На собеседованиях вопросы про Span появились как только он вышел в .NET Core 2.1+, и в .NET 9 всё чаще встречается как желаемое знание.
Визуальная схема: где Span, а где массив
+--------------------+
| int[] массив |
| 1 2 3 4 5 6 7 8 |
+--------------------+
^ ^
| |
[ 2, 3, 4, 5 ] <-- Span<int> "окно памяти" (slice)
Span<T> — это не отдельный массив, а “прозрачная линза” на часть данных.
Отличие от других коллекций: таблица сравнения
| Тип | Хранит данные? | Можно менять элементы? | Можно менять размер? | Копирует при срезе? | Где живёт? |
|---|---|---|---|---|---|
|
Да | Да | Нет | Да (через .Take) | Heap |
|
Да | Да | Да | Да | Heap |
|
Нет | Да | Нет | Нет | Stack |
|
Нет | Нет | Нет | Нет | Stack |
9. Типичные ошибки при работе с Span/ReadOnlySpan
Ошибка №1: попытка сохранить Span как поле класса. Компилятор выдаст ошибку “Span type may not be used in this context”. Это сделано намеренно: хранение Span в поле небезопасно.
Ошибка №2: возврат Span из async-метода. Так делать нельзя, потому что async-методы могут “убежать” на кучу. Вместо этого используйте массив или другой тип.
Ошибка №3: забывают, что изменения через Span отражаются на исходном массиве. Это может неожиданно менять данные “снаружи” и приводить к непредсказуемому поведению.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ