OOP Course
Сьогодні

Підрозділ 13.5

Span<T> та ReadOnlySpan<T>

Розглядає Span і ReadOnlySpan як подання ділянки пам'яті без зайвого копіювання: створення з масиву, Slice, спільні дані, основні методи й обмеження ref struct.

13.5. `Span` та `ReadOnlySpan`

Коли медична інформаційна система обробляє тисячі CSV-рядків із результатами аналізів або парсить бінарний потік HL7 MLLP, кожен виклик string.Substring() або arr[2..5] виділяє нову ділянку пам'яті у heap і копіює туди дані. При великих обсягах — мільйони дрібних об'єктів, надмірне навантаження на збирач сміття (GC), непередбачувані паузи.

Span<T> вирішує цю проблему принципово: він не виділяє пам'ять і не копіює дані — він лише вказує на вже існуючу ділянку пам'яті (масив, рядок, stackalloc-буфер) і зберігає довжину. Весь доступ до даних відбувається через цей вказівник без жодного копіювання.

Span<T> — безкопійний доступ до пам'яті

Проблема копіювання: Substring vs Span

Розглянемо рядок "P001,Іван,67,I10.9". Щоб отримати поле "Іван" традиційним способом:

string line = "P001,Іван,67,I10.9";
string name = line.Substring(5, 4); // нова ділянка heap + копіювання 4 символів

Substring завжди створює новий рядок на heap. При парсингу 100 000 рядків — 100 000 дрібних рядків у heap, щоразу виділяються і збираються GC.

З ReadOnlySpan<char>:

ReadOnlySpan<char> span = line.AsSpan(5, 4);
// heap не виділяється — span вказує на символи [5..8] оригінального рядка

span живе на стеку і містить лише два значення: вказівник на пам'ять і довжину. Оригінальний рядок не змінюється, копіювання не відбувається.

Що таке Span

Span<T> — це ref struct, тобто структура, яка завжди знаходиться на стеку. Вона може вказувати на:

  • масив: new int[]{1,2,3}.AsSpan()
  • частину масиву: arr.AsSpan(2, 5)
  • рядок: str.AsSpan()
  • буфер на стеку: stackalloc int[16]
  • неупорядковану пам'ять (через MemoryMarshal)
int[] data = { 10, 20, 30, 40, 50 };

Span<int> all    = data.AsSpan();         // весь масив
Span<int> middle = data.AsSpan(1, 3);     // { 20, 30, 40 } без копії

middle[0] = 99; // змінює data[1]!
Console.WriteLine(data[1]); // 99 — Span<T> є вікном у оригінальні дані

Span<T>змінний: через нього можна не лише читати, але й записувати дані. Запис через Span змінює оригінальну ділянку пам'яті.

ReadOnlySpan — незмінний варіант

ReadOnlySpan<T> — аналог Span<T>, але тільки для читання. Рядки C# незмінні, тому string.AsSpan() повертає саме ReadOnlySpan<char>:

string s = "Петренко Іван";
ReadOnlySpan<char> span = s.AsSpan();

char first = span[0];   // 'П'
// span[0] = 'X';       // помилка компіляції — ReadOnly

ReadOnlySpan<char> є найпоширенішим застосуванням Span: парсинг рядків без виділення підрядків.

API Span

Індексація та довжина:

Span<int> span = new int[]{ 10, 20, 30, 40, 50 }.AsSpan();

int val    = span[2];    // 30
span[2]    = 99;         // запис
int length = span.Length; // 5

Slice — безкопійний підзріз:

Span<int> sub = span.Slice(1, 3); // { 20, 99, 40 } — вказівник, не копія

CopyTo — копіювання у масив або інший Span:

int[] dest = new int[3];
span.Slice(1, 3).CopyTo(dest); // копіювати 3 елементи у dest

Fill та Clear:

span.Fill(0);  // заповнити нулями
span.Clear();  // теж 0 (для числових), false для bool, null для ref

SequenceEqual — порівняння вмісту:

ReadOnlySpan<char> a = "hello".AsSpan();
ReadOnlySpan<char> b = "hello".AsSpan();
Console.WriteLine(a.SequenceEqual(b)); // true

Парсинг рядків через ReadOnlySpan

Ключова перевага ReadOnlySpan<char> — методи int.TryParse, double.TryParse та інші приймають ReadOnlySpan<char> як аргумент, дозволяючи парсити без виділення підрядка:

ReadOnlySpan<char> numSpan = "67".AsSpan();
int.TryParse(numSpan, out int age); // age = 67, heap не виділявся

Для парсингу CSV-рядка поле за полем:

ReadOnlySpan<char> line = "P001,Іван,67,I10.9".AsSpan();

int comma1 = line.IndexOf(',');                          // 4
ReadOnlySpan<char> id   = line.Slice(0, comma1);         // "P001"
ReadOnlySpan<char> rest = line.Slice(comma1 + 1);        // "Іван,67,I10.9"

int comma2 = rest.IndexOf(',');
ReadOnlySpan<char> name = rest.Slice(0, comma2);         // "Іван"
rest = rest.Slice(comma2 + 1);                           // "67,I10.9"

int comma3 = rest.IndexOf(',');
ReadOnlySpan<char> ageSpan = rest.Slice(0, comma3);      // "67"
int.TryParse(ageSpan, out int age);                      // 67

Жоден новий рядок на heap не виділяється — лише вказівники на ділянки оригінального буфера.

Обмеження ref struct

Span<T> є ref struct — це технічне обмеження, що забезпечує гарантію перебування на стеку. Наслідки:

  • Не можна зберігати в полі класу: ref struct не може бути частиною heap-об'єкта.
  • Не можна в async/await методах: стек-фрейм async-методу переноситься між потоками, ref struct цього не підтримує.
  • Не можна боксувати: object o = span — помилка компіляції.
  • Не можна в масиві: Span<T>[] неможливо.
  • LINQ не підтримується: більшість LINQ-методів приймають IEnumerable<T>, але Span<T> не реалізує цей інтерфейс. foreach — працює.

Якщо потрібно зберегти посилання на дані між викликами методів або в полі класу — використовується Memory<T> (змінний) або ReadOnlyMemory<T> (незмінний). Memory<T> — це heap-обгортка, що всередині зберігає Span<T>, але без обмежень ref struct.

Парсинг CSV без Split — runnable приклад

Демонстрація ReadOnlySpan<char> для парсингу рядка без виділення підрядків:

Span для підмасиву вимірювань — runnable приклад

Розроблено Tomka Yurii · © 2026 ·