Підрозділ 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-буфер) і зберігає довжину. Весь доступ до даних відбувається через цей вказівник без жодного копіювання.

Проблема копіювання: 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'; // помилка компіляції — ReadOnlyReadOnlySpan<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; // 5Slice — безкопійний підзріз:
Span<int> sub = span.Slice(1, 3); // { 20, 99, 40 } — вказівник, не копіяCopyTo — копіювання у масив або інший Span:
int[] dest = new int[3];
span.Slice(1, 3).CopyTo(dest); // копіювати 3 елементи у destFill та Clear:
span.Fill(0); // заповнити нулями
span.Clear(); // теж 0 (для числових), false для bool, null для refSequenceEqual — порівняння вмісту:
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> для парсингу рядка без виділення підрядків: