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? |
|---|---|---|---|---|---|
|
Tak | Tak | Nie | Tak (przez .Take) | Heap |
|
Tak | Tak | Tak | Tak | Heap |
|
Nie | Tak | Nie | Nie | Stack |
|
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.
GO TO FULL VERSION