CodeGym /Kursy /C# SELF /Wprowadzenie do Span<T&...

Wprowadzenie do Span<T> i ReadOnlySpan<T>

C# SELF
Poziom 65 , Lekcja 2
Dostępny

1. Wprowadzenie

Kiedy pracujesz z tablicami, łańcuchami i buforami bajtów, często pojawia się zadanie „obejrzeć” część tych danych. Na przykład wydzielić podłańcuch, wziąć wycinek tablicy, przetworzyć część przychodzącego strumienia. W starych wersjach .NET trzeba było albo kopiować dane (tworzyć nową tablicę/podłańcuch), albo pisać kod iterujący tablicę od jednego indeksu do drugiego. To wszystko nie jest zbyt przyjemne ani pod względem wydajności, ani czytelności.

Oto przykład starego podejścia: trzeba przekazać do metody tylko część dużej tablicy:

// Stare podejście — kopiujemy część tablicy (niewydajne!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // tworzy się nowa tablica

W związku z tym, jeżeli trzeba efektywnie przekazywać „wycinek” tablicy (albo nawet kawałek stringa), bez tworzenia zbędnych obiektów, stare środki C# ewidentnie przegrywają, szczególnie przy dużych ilościach danych.

Właśnie tutaj na pomoc przychodzi bohater dnia — Span<T>!

2. Co to jest Span<T>? Główna idea

Span<T> to typ reprezentujący ciągły obszar pamięci jednego typu T. Jego zadaniem jest dać szybki, bezpieczny i wydajny sposób pracy z kawałkami tablic, łańcuchów, struktur oraz z pamięcią niezarządzaną (np. pamięcią zaalokowaną poza środowiskiem zarządzanym .NET).

Główna zaleta Span<T> to „wycinki bez tworzenia nowych tablic”. Wyobraź sobie linijkę, dzięki której możesz mierzyć dowolne fragmenty tej samej tablicy bez kopiowania danych i z minimalnym ryzykiem pomyłki w indeksach.

Krótko:

  • Span<T> — „okno” albo „widok” na kawałek pamięci, którym można wygodnie i bezpiecznie manipulować.
  • Brak alokacji nowej pamięci — oszczędność zasobów i mniej pracy dla GC.
  • Działa nie tylko z tablicami, ale też z fragmentami stringów, blokami stackalloc i nawet z pamięcią niezarządzaną.
  • Nie można przechowywać w polach zwykłej klasy: to typ stosowy (stack-only struct).

Dlaczego to ważne?

W zadaniach o wysokiej wydajności (parsowanie plików, przetwarzanie dużych buforów, kryptografia, serializacja) oszczędność nawet kilku kopiowań tablic może dać ogromny przyrost szybkości i zmniejszyć obciążenie garbage collectora (GC). A dodatkowo pokażesz kolegom, że znasz nowoczesny C# i .NET!

3. Podstawowe użycie Span<T>: pierwszy wycinek

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

// Utworzyć Span na część tablicy (np. elementy od 2 do 5 włącznie)
Span<int> middle = new Span<int>(numbers, 2, 4); // indeksy: 2, 3, 4, 5

// Widzimy podtablicę: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6

// Zmiana przez Span zmienia oryginalną tablicę!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999

Ważne! Span<T> nie kopiuje danych, tylko wskazuje na „kawałek” tablicy. Wszystkie zmiany są widoczne i w oryginalnej tablicy, i w Span.

4. Główne sposoby tworzenia Span<T>

Na podstawie tablicy:

int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // pełna długość
Span<int> slice = arr.AsSpan(1, 3); // elementy 20, 30, 40

Na podstawie części tablicy:

Span<int> part = new Span<int>(arr, 2, 2); // elementy 30, 40

stackalloc: alokacja pamięci na stosie (bardzo szybko i nie trafia do „sterfy heap”):

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

Za pomocą metody .Slice():

Span<int> subSpan = span.Slice(1, 2); // elementy 20, 30

Wizualny schemat „wycinka”


Początkowa tablica:  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
                           <--- Span: 3, 4, 5, 6 --->

5. Ograniczenia i cechy Span<T>

  • Stack-only! Nie można przechowywać w „zwykłych” polach klasy ani używać jako części zamknięcia — to typ stosowy.
  • Nie można używać jako pola klasy ani zwracać z async-metod (kompilator zgłosi błąd).
  • Nie można przechwytywać w lambda/anonimowych metodach — używaj „tu i teraz”.
  • Nie można bezpośrednio serializować ani przekazywać między wątkami.

To wynika z faktu, że Span może wskazywać na dowolny obszar pamięci, i jeśli nagle „przeniesie się” do heapu, można dostać niebezpieczne stany.

6. Niemutowalność: ReadOnlySpan<T>

Czasami trzeba „spojrzeć” na część pamięci, ale nie zmieniać jej. Do tego jest niemutowalna wersja — 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'; // Błąd: indeksator tylko do odczytu!

Klasyczny scenariusz — bezpieczne przekazanie „kawałka” stringa lub tablicy tam, gdzie nie powinno się (i nie chce się) go zmieniać.

7. Praktyka na przykładzie: „Wycinamy” tablice i stringi

Załóżmy, to analyzer danych, który z długiego stringa wydziela podłańcuch, szuka w nim liczb i zwraca ich sumę (bez zbędnych kopiowań przy początkowym wycinku):

using System;

class Program
{
    static void Main()
    {
        // Załóżmy, użytkownik wpisał długi string liczb rozdzielonych spacjami
        string input = "12 34 56 78 90 123 456 789";
        // Musimy policzyć sumę liczb tylko ze "środka", np. 56 78 90

        // Wydzielamy podłańcuch (ale go nie kopiujemy!)
        ReadOnlySpan<char> center = input.AsSpan(6, 8); // indeksy można obliczać dynamicznie

        // Parsujemy liczby przez Split (tworzy się tymczasowa tablica)
        string[] numbers = center.ToString().Split(' ');
        int sum = 0;
        foreach (var str in numbers)
        {
            if (int.TryParse(str, out int num))
                sum += num;
        }
        Console.WriteLine($"Suma środkowych liczb: {sum}");
    }
}

Nowoczesne biblioteki parsujące CSV i JSON używają Span dla wysokiej prędkości pracy z dużymi ilościami tekstu — teraz wiesz, na czym opiera się ich „magia”.

8. Przydatne niuanse

Span kontra kopiowanie tablic

// Stary sposób: kopiujemy fragment tablicy
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // utworzono nową tablicę 1000 elementów

// Nowy sposób: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // wcale nie kopiujemy!
bestThousand[0] = 42; // zmienia się też w arr

Różnica jest szczególnie widoczna przy intensywnym parsowaniu plików, przetwarzaniu buforów sieciowych, pracy z danymi binarnymi.

Zastosowanie w realnych zadaniach: po co znać Span

  • Wysokowydajne parsowanie i przetwarzanie tekstowych/binarnych danych. Nowoczesne biblioteki serializacji (np. System.Text.Json, Span w dokumentacji Microsoft) używają Span, żeby przyspieszyć pracę.
  • Buforowanie i odczyt plików (dzielenie dużych buforów bez kopiowania).
  • Przetwarzanie danych tam, gdzie liczy się ograniczona pamięć (embedded, IoT) — oficjalna dokumentacja o Memory/Spans.
  • Algorytmy do obróbki obrazów i audio, gdzie ważna jest prędkość i brak zbędnych alokacji.
  • Przyspieszenie parsowania CSV, JSON, XML za pomocą Span — zwłaszcza w .NET 8/9.

Na rozmowach rekrutacyjnych pytania o Span pojawiły się, gdy wyszedł w .NET Core 2.1+, a w .NET 9 coraz częściej jest to pożądana wiedza.

Wizualny schemat: gdzie Span, a gdzie tablica


+--------------------+
|   int[]  tablica   |
|  1 2 3 4 5 6 7 8   |
+--------------------+
       ^       ^
       |       |
   [ 2, 3, 4, 5 ]  <-- Span<int> "okno pamięci" (slice)

Span<T> — to nie jest osobna tablica, a „przezroczysta soczewka” na część danych.

Różnica względem innych kolekcji: porównanie

Typ Przechowuje dane? Można zmieniać elementy? Można zmieniać rozmiar? Kopiuje przy wycinku? Gdzie żyje?
int[]
Tak Tak Nie Tak (przez .Take) Heap
List<int>
Tak Tak Tak Tak Heap
Span<int>
Nie Tak Nie Nie Stack
ReadOnlySpan<int>
Nie Nie Nie Nie Stack

9. Typowe błędy przy pracy z Span/ReadOnlySpan

Błąd nr 1: próba zapisania Span jako pola klasy. Kompilator zgłosi błąd „Span type may not be used in this context”. To jest celowe: przechowywanie Span w polu jest niebezpieczne.

Błąd nr 2: zwracanie Span z async-metody. Nie wolno tego robić, bo async-metody mogą „uciec” na heap. Zamiast tego użyj tablicy lub innego typu.

Błąd nr 3: zapomnienie, że zmiany przez Span wpływają na oryginalną tablicę. To może niespodziewanie zmieniać dane „na zewnątrz” i prowadzić do nieprzewidywalnego zachowania.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION