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 відображаються на вихідному масиві. Це може неочікувано змінювати дані «ззовні» та призводити до непередбачуваної поведінки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ