JavaRush /Курсы /C# SELF /Новые возможности Span<...

Новые возможности 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>?

Начнём с простого массива и создадим спан:

int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // с 3-го элемента (индекс 2) взять 3 элемента

Теперь midSpan указывает на элементы 30, 40, 50 — это не копия, а взгляд на "живые" элементы массива. Изменяя их через спан, меняем исходный массив.

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<T> меняет исходный массив. Если нужна независимая копия — явно скопируйте данные.

2
Задача
C# SELF, 65 уровень, 3 лекция
Недоступна
Использование диапазонов и индекса с конца для отображения части массива
Использование диапазонов и индекса с конца для отображения части массива
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ