JavaRush /Курсы /C# SELF /Введение в Span<T>

Введение в Span<T> и ReadOnlySpan<T>

C# SELF
65 уровень , 2 лекция
Открыта

1. Введение

Когда вы работаете с массивами, строками и байтовыми буферами, часто возникает задача “посмотреть” на часть этих данных. Например, выделить подстроку, взять срез массива, обработать часть входящего потока. В старых версиях .NET для этого приходилось либо копировать данные (создавать новый массив/подстроку), либо писать код, обходя массив с нужного индекса до нужного конца. Всё это не слишком приятно ни по производительности, ни по читаемости.

Вот пример старого подхода: нам нужно передать в метод только часть большого массива:

// Старый подход — копируем часть массива (неэффективно!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // создаётся новый массив

Таким образом, если нам нужно эффективно передавать “срез” массива (или даже куска строки), не создавая лишние объекты, старые средства C# явно проигрывают, особенно если у нас дело с большими объёмами информации.

Вот тут-то и приходит на помощь герой дня — Span<T>!

2. Что такое Span<T>? Основная идея

Span<T> — это тип, который представляет непрерывную область памяти одного и того же типа T. Его задача — дать вам быстрый, безопасный и эффективный способ работы с кусками массивов, строк, структур, а также с неуправляемой памятью (например, память, выделенная вне управляемой среды .NET).

Главная фишка Span<T> — это “срезы без создания новых массивов”. Представьте линейку, с помощью которой вы можете измерять любые участки одного и того же массива без копирования данных и с минимальным риском ошибиться в индексах.

Кратко:

  • Span<T> — “окно” или “вид” на кусок памяти, которым можно удобно и безопасно манипулировать.
  • Нет выделения новой памяти — экономия ресурсов и меньше работы для GC.
  • Работает не только с массивами, но и с сегментами строк, stackalloc-блоками и даже с неуправляемой памятью.
  • Нельзя хранить в полях обычного класса: это стековый тип (stack-only struct).

Почему это важно?

В высокопроизводительных задачах (парсинг файлов, обработка больших буферов, криптография, сериализация) экономия даже пары копирований массивов может дать огромный прирост скорости и снизить нагрузку на систему сборки мусора (GC). А ещё вы просто покажете коллегам, что следите за современным C# и .NET!

3. Базовое использование Span<T>: первый срез

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Создать Span на часть массива (например, элементы со 2 по 5 включительно)
Span<int> middle = new Span<int>(numbers, 2, 4); // индексы: 2, 3, 4, 5

// Мы видим подмассив: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6

// Изменение Span изменяет исходный массив!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999

Важно! Span<T> не копирует данные, а лишь указывает на “кусок” массива. Все изменения видны и в исходном массиве, и в Span.

4. Основные способы создания Span<T>

На основе массива:

int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // полная длина
Span<int> slice = arr.AsSpan(1, 3); // элементы 20, 30, 40

На основе части массива:

Span<int> part = new Span<int>(arr, 2, 2); // элементы 30, 40

stackalloc: выделение памяти на стеке (крайне быстро и не попадает в “кучу”):

Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;

С помощью метода .Slice():

Span<int> subSpan = span.Slice(1, 2); // элементы 20, 30

Визуальная схема “среза”


Изначальный массив:  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
                           <--- Span: 3, 4, 5, 6 --->

5. Ограничения и особенности Span<T>

  • Stack-only! Нельзя хранить в “обычных” полях класса или делать частью замыкания — это стековый тип.
  • Нельзя использовать как поле класса или возвращать из async-методов (компилятор выдаст ошибку).
  • Нельзя захватывать в лямбдах/анонимных методах — используйте “здесь и сейчас”.
  • Нельзя напрямую сериализовать или передавать между потоками.

Это связано с тем, что Span может указывать на любую область памяти, и если он вдруг “переедет” в кучу, можно получить небезопасные состояния.

6. Иммутабельность: ReadOnlySpan<T>

Иногда нужно “взглянуть” на часть памяти, но не изменять её. Для этой задачи есть неизменяемый вариант — ReadOnlySpan<T>.

string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n

// letters[0] = 'Z'; // Ошибка: индексатор только для чтения!

Классический сценарий — безопасная передача “куска” строки или массива куда-то, где его нельзя (и не хочется) менять.

7. Практика на примере: “Срезаем” массивы и строки

Допустим, это анализатор данных, который из большой строки выделяет подстроку, ищет в ней числа и возвращает их сумму (без лишних копирований при первичном срезе):

using System;

class Program
{
    static void Main()
    {
        // Допустим, пользователь ввёл длинную строку чисел, разделённых пробелами
        string input = "12 34 56 78 90 123 456 789";
        // Нам нужно рассчитать сумму чисел только из "центра", например, 56 78 90

        // Выделяем подстроку (но не копируем её!)
        ReadOnlySpan<char> center = input.AsSpan(6, 8); // индексы можно вычислить динамически

        // Парсим числа через Split (создаём временный массив)
        string[] numbers = center.ToString().Split(' ');
        int sum = 0;
        foreach (var str in numbers)
        {
            if (int.TryParse(str, out int num))
                sum += num;
        }
        Console.WriteLine($"Сумма центральных чисел: {sum}");
    }
}

Современные библиотеки парсинга CSV и JSON используют Span для высокой скорости работы с большими объёмами строковых данных — теперь вы знаете, на чём основано их “волшебство”.

8. Полезные нюансы

Span против копирования массивов

// Старый способ: копируем кусок массива
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // создали новый массив на 1000 элементов

// Новый способ: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // не копируем вообще!
bestThousand[0] = 42; // меняется и в arr

Особенно разница заметна при интенсивных парсингах файлов, обработке сетевых буферов, работе с бинарными данными.

Применение в реальных задачах: зачем знать про Span

  • Высокопроизводительный парсинг и обработка строковых/бинарных данных. Современные библиотеки сериализации (например, System.Text.Json, Span в документации Microsoft) используют Span для ускорения работы.
  • Буферизация и чтение файлов (разрезаем большие буферы без копирования).
  • Обработка данных с ограничением по памяти (embedded, IoT) — официальная документация по Memory/Spans.
  • Алгоритмы работы с изображениями и аудио, где важна скорость и отсутствие лишних аллокаций.
  • Ускорение парсинга CSV, JSON, XML с помощью Span — особенно в .NET 8/9.

На собеседованиях вопросы про Span появились как только он вышел в .NET Core 2.1+, и в .NET 9 всё чаще встречается как желаемое знание.

Визуальная схема: где Span, а где массив


+--------------------+
|   int[]  массив    |
|  1 2 3 4 5 6 7 8   |
+--------------------+
       ^       ^
       |       |
   [ 2, 3, 4, 5 ]  <-- Span<int> "окно памяти" (slice)

Span<T> — это не отдельный массив, а “прозрачная линза” на часть данных.

Отличие от других коллекций: таблица сравнения

Тип Хранит данные? Можно менять элементы? Можно менять размер? Копирует при срезе? Где живёт?
int[]
Да Да Нет Да (через .Take) Heap
List<int>
Да Да Да Да Heap
Span<int>
Нет Да Нет Нет Stack
ReadOnlySpan<int>
Нет Нет Нет Нет Stack

9. Типичные ошибки при работе с Span/ReadOnlySpan

Ошибка №1: попытка сохранить Span как поле класса. Компилятор выдаст ошибку “Span type may not be used in this context”. Это сделано намеренно: хранение Span в поле небезопасно.

Ошибка №2: возврат Span из async-метода. Так делать нельзя, потому что async-методы могут “убежать” на кучу. Вместо этого используйте массив или другой тип.

Ошибка №3: забывают, что изменения через Span отражаются на исходном массиве. Это может неожиданно менять данные “снаружи” и приводить к непредсказуемому поведению.

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