JavaRush /Курси /C# SELF /Вступ до Span<T>

Вступ до Span<T> і ReadOnlySpan<T>

C# SELF
Рівень 65 , Лекція 2
Відкрита

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> — це не окремий масив, а «прозора лінза» на частину даних.

Відмінність від інших колекцій: таблиця порівняння

Тип Зберігає дані? Можна змінювати елементи? Можна змінювати розмір? Копіює при зрізі? Де живе?
int[]
Так Так Ні Так (через .Take) Heap
List<int>
Так Так Так Так Heap
Span<int>
Ні Так Ні Ні Stack
ReadOnlySpan<int>
Ні Ні Ні Ні Stack

9. Типові помилки при роботі з Span/ReadOnlySpan

Помилка № 1: спроба зберегти Span як поле класу. Компілятор видасть помилку «Span type may not be used in this context». Це зроблено навмисно: зберігання Span у полі небезпечно.

Помилка № 2: повернення Span з async-методу. Так робити не можна, адже async-методи можуть «утекти» в купу. Замість цього використовуйте масив або інший тип.

Помилка № 3: забувають, що зміни через Span відображаються на вихідному масиві. Це може неочікувано змінювати дані «ззовні» та призводити до непередбачуваної поведінки.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ