JavaRush /Курси /C# SELF /Нові можливості Span<T&...

Нові можливості Span<T>

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

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

Тип Змінюваний Копіює дані? Можна у стеку Клас/структура Можна робити поля класу
T[]
так - ні клас так
ArraySegment<T>
так ні ні структура так
Span<T>
так ні так структура ні
ReadOnlySpan<T>
ні ні так структура ні

Іншими словами, Span<T> — це еволюція ArraySegment<T>: продуктивніше й безпечніше.

8. Типові помилки та підводні камені

Спроба зберігати Span<T> у полі класу. Не можна: поля живуть у купі, а Span<T> має жити у стеку. Використовуйте ArraySegment<T> або індекси для адресації.

Повернення/зберігання Span<T> з async-методів. Не можна: асинхронність розриває стек. Передавайте дані інакше — наприклад, як масив, Memory<T>/ReadOnlyMemory<T>.

Некоректний діапазон під час створення зрізу. Вихід за межі призводить до винятку під час виконання. Завжди перевіряйте довжину та межі перед формуванням зрізу.

Забули, що Span відображає вихідні дані. Будь-яка зміна через Span<T> змінює вихідний масив. Якщо потрібна незалежна копія — створіть її явно.

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