1. Вступ
Уявімо, що у вас є великий масив даних, наприклад байти, зчитані з файлу. Ви хочете обробити його фрагмент — прочитати частину як рядок або просто передати ділянку даних методу. У «старому» стилі ви б копіювали ці байти в новий масив — це повільно й споживає багато памʼяті. А якщо таких фрагментів — десятки чи сотні?
І саме тут зʼявляється рішення — Span<T>. Це особлива структура, яка описує зріз (view) над масивом (або навіть над будь-яким безперервним фрагментом памʼяті), не копіюючи дані, а лише вказуючи на потрібну область. Важливо: Span<T> — це не нова колекція, а безпечне «вікно» в наявний масив!
Основні особливості та обмеження Span<T>
- Span<T> — це структура (value type), яка є «вікном» у памʼять і не копіює елементи.
- Вказує лише на безперервну ділянку памʼяті: масив, частину масиву, stackalloc-блок, памʼять рядка через спеціальні методи або памʼять в unsafe-коді.
- Span<T> живе у стеку. Його не можна зберігати в полі класу або безпосередньо повертати з async-методів.
- Гарантує безпеку типів: доступ до памʼяті — без ручної роботи з вказівниками (якщо ви не використовуєте unsafe).
Коротко про нові можливості C# 14
C# 14 розширює роботу зі зрізами: зручні діапазони .. та індекси з кінця ^1, удосконалені патерни зіставлення для масивів і зрізів та інший «синтаксичний цукор». До цього повернемося наприкінці.
2. Як створити Span<T>?
Почнімо з простого масиву й створімо Span:
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // з 3-го елемента (індекс 2) узяти 3 елементи
Тепер midSpan вказує на елементи 30, 40, 50 — це не копія, а погляд на «живі» елементи масиву. Змінюючи їх через Span, змінюємо вихідний масив.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Виведе 100
Чому не використовувати зрізи масивів безпосередньо?
Класичний LINQ-зріз створює новий масив:
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- тут створюється копія!
Це витратно за часом і памʼяттю. Span розвʼязує проблему зайвих копій — і працює не лише з числами.
3. Ще більше способів створити Span<T>
Наявний масив
Span<int> mySpan = numbers; // Неявне перетворення з масиву до Span
Частина масиву
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 елементи, починаючи з індексу 1: {20, 30, 40, 50}
Стекова памʼять (stackalloc)
Span<T> дозволяє виділяти масиви у стеку:
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
Стекова памʼять швидка й звільняється автоматично під час виходу з методу. Але обсяг має бути розумним — мегабайти у стеку неприпустимі.
Рядки та ReadOnlySpan<char>
Рядки у .NET незмінні, тому використовуємо ReadOnlySpan<char>:
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Зберімо приклад!
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Уся історія замовлень: ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("Виведемо замовлення з 2-го по 4-й (індекси 1-3):");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Змінюємо дані через Span
recentOrders[1] = 999;
Console.WriteLine("Після зміни через Span:");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Зріз recentOrders справді працює безпосередньо з масивом — це не копія.
5. Безпека, продуктивність і перевірки
- Ощадливе використання памʼяті: жодних зайвих копій.
- Перевірки меж захищають від виходу за межі масиву.
- JIT-оптимізації забезпечують дуже швидкий доступ (без unsafe).
Взаємодія з методами
Методи можуть приймати Span<T> або ReadOnlySpan<T>. Якщо змінювати дані не планується — використовуйте ReadOnlySpan<T>.
static int Sum(Span<int> slice)
{
int sum = 0;
foreach (var item in slice)
sum += item;
return sum;
}
int[] data = { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(data.AsSpan(2, 3))); // 3+4+5=12
6. Використання діапазонів (range)
Починаючи з C# 8, доступні діапазони .. та індекси з кінця ^ — вони чудово працюють зі зрізами.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // індекси 2,3,4 — тобто 30, 40, 50
У діапазоні початковий індекс включається, кінцевий — ні.
Індекси з кінця
int lastElement = arr[^1]; // останній елемент (60)
Span<int> lastTwo = arr[^2..]; // два останні елементи (50, 60)
Діапазони й рядки
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Цей зріз — це ReadOnlySpan<char>; щоб отримати рядок, викличте ToString().
7. Сучасні патерни роботи зі зрізами (C# 14)
Нові патерни полегшують аналіз масивів і зрізів.
if (arr is [10, 20, .. var rest]) // .. захоплює "хвіст" масиву
{
Console.WriteLine("Початок збігся, хвіст:");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("Масив закінчується на 50, 60");
Порівняння: масив, ArraySegment і Span
| Тип | Змінюваний | Копіює дані? | Можна у стеку | Клас/структура | Можна робити поля класу |
|---|---|---|---|---|---|
|
так | - | ні | клас | так |
|
так | ні | ні | структура | так |
|
так | ні | так | структура | ні |
|
ні | ні | так | структура | ні |
Іншими словами, Span<T> — це еволюція ArraySegment<T>: продуктивніше й безпечніше.
8. Типові помилки та підводні камені
Спроба зберігати Span<T> у полі класу. Не можна: поля живуть у купі, а Span<T> має жити у стеку. Використовуйте ArraySegment<T> або індекси для адресації.
Повернення/зберігання Span<T> з async-методів. Не можна: асинхронність розриває стек. Передавайте дані інакше — наприклад, як масив, Memory<T>/ReadOnlyMemory<T>.
Некоректний діапазон під час створення зрізу. Вихід за межі призводить до винятку під час виконання. Завжди перевіряйте довжину та межі перед формуванням зрізу.
Забули, що Span відображає вихідні дані. Будь-яка зміна через Span<T> змінює вихідний масив. Якщо потрібна незалежна копія — створіть її явно.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ