1. Wprowadzenie
Wyobraź sobie, że masz dużą tablicę danych, na przykład bajty odczytane z pliku. Chcesz przetworzyć jej fragment — odczytać część jako string albo po prostu przekazać kawałek danych do metody. W "starym" stylu kopiowałbyś te bajty do nowej tablicy — to wolne i pamięciożerne. A jeśli takich fragmentów są dziesiątki albo setki?
I tu pojawia się bohater — Span<T>. To specjalna struktura, która opisuje wycinek (view) nad tablicą (albo nawet nad dowolnym ciągłym obszarem pamięci), bez kopiowania danych, po prostu wskazując odpowiedni zakres. Ważne: Span<T> to nie nowa kolekcja, a bezpieczne "okno" w istniejącą tablicę!
Główne cechy i ograniczenia Span<T>
- Span<T> to struktura (value type), reprezentująca "okno" w pamięć i nie kopiująca elementów.
- Odwołuje się tylko do ciągłego obszaru pamięci: tablica, część tablicy, blok z stackalloc, pamięć stringa przez specjalne metody, albo pamięć w unsafe-kodzie.
- Span<T> żyje na stosie. Nie wolno przechowywać go w polu klasy ani zwracać bezpośrednio z async-metod.
- Gwarantuje bezpieczeństwo typów: dostęp do pamięci — bez ręcznej pracy z wskaźnikami (o ile nie używasz unsafe).
Krótko o nowych możliwościach C# 14
C# 14 rozszerza pracę z wycinkami: wygodne zakresy .. i indeksy od końca ^1, ulepszone patterny dopasowania dla tablic i wycinków oraz inne "cukierki" składniowe. Do tego wrócimy na końcu.
2. Jak stworzyć Span<T>?
Zacznijmy od prostej tablicy i stwórzmy span:
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // od 3-ciego elementu (indeks 2) wziąć 3 elementy
Teraz midSpan wskazuje elementy 30, 40, 50 — to nie kopia, tylko widok na "żywe" elementy tablicy. Zmieniając je przez span, zmieniasz oryginalną tablicę.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Wypisze 100
Dlaczego nie używać wycinków tablic bezpośrednio?
Klasyczny wycinek LINQ tworzy nową tablicę:
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- tutaj powstaje kopia!
To kosztuje czas i pamięć. Span rozwiązuje problem zbędnych kopii — i działa nie tylko z liczbami.
3. Jeszcze więcej sposobów tworzenia Span<T>
Istniejąca tablica
Span<int> mySpan = numbers; // Niejawna konwersja z tablicy na Span
Część tablicy
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 elementy zaczynając od indeksu 1: {20, 30, 40, 50}
Pamięć stosu (stackalloc)
Span<T> pozwala alokować tablice na stosie:
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
Pamięć stosu jest szybka i zwalniana automatycznie po wyjściu z metody. Ale rozmiar powinien być rozsądny — megabajty na stosie są niedopuszczalne.
Stringi i ReadOnlySpan<char>
Stringi w .NET są niemutowalne, więc używamy ReadOnlySpan<char>:
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Złóżmy przykład!
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Cała historia zamówień: ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("Wypisać zamówienia od 2 do 4 (indeksy 1-3):");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Mutujemy dane przez Span
recentOrders[1] = 999;
Console.WriteLine("Po zmianie przez Span:");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Wycinek recentOrders działa bezpośrednio z tablicą — to nie kopia.
5. Bezpieczeństwo, wydajność i sprawdzenia
- Oszczędne gospodarowanie pamięcią: brak zbędnych kopii.
- Sprawdzenia zakresów chronią przed wyjściem poza tablicę.
- Optymalizacje JIT dają bardzo szybki dostęp (bez unsafe).
Interakcja z metodami
Metody mogą przyjmować Span<T> lub ReadOnlySpan<T>. Jeśli nie planujesz modyfikować danych — użyj 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. Użycie zakresów (range)
Począwszy od C# 8 dostępne są zakresy .. i indeksy od końca ^ — świetnie działają ze wycinkami.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // indeksy 2,3,4 - czyli 30, 40, 50
W zakresie indeks początkowy jest włączony, końcowy — wyłączony.
Indeksy od końca
int lastElement = arr[^1]; // ostatni element (60)
Span<int> lastTwo = arr[^2..]; // dwa ostatnie elementy (50, 60)
Zakresy i stringi
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Ten wycinek to ReadOnlySpan<char>; żeby dostać string, wywołaj ToString().
7. Nowoczesne patterny pracy z wycinkami (C# 14)
Nowe patterny upraszczają rozbiórkę tablic i wycinków.
if (arr is [10, 20, .. var rest]) // .. przechwytuje "ogon" tablicy
{
Console.WriteLine("Początek pasuje, ogon:");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("Tablica kończy się na 50, 60");
Porównanie: Tablica, ArraySegment i Span
| Typ | Mutowalny | Kopiuje dane? | Można na stosie | Klasa/struktura | Można robić pola klasy |
|---|---|---|---|---|---|
|
tak | - | nie | klasa | tak |
|
tak | nie | nie | struktura | tak |
|
tak | nie | tak | struktura | nie |
|
nie | nie | tak | struktura | nie |
Innymi słowy, Span<T> to ewolucja ArraySegment<T>: bardziej wydajny i bezpieczny.
8. Typowe błędy i pułapki
Próba przechowywania Span<T> w polu klasy. Nie wolno: pola żyją na heapie, a Span<T> musi żyć na stosie. Użyj ArraySegment<T> albo indeksów do adresowania.
Zwracanie/przechowywanie Span<T> z async-metod. Nie wolno: asynchroniczność przerywa stos. Przekaż dane inaczej — np. jako tablicę, Memory<T>/ReadOnlyMemory<T>.
Nieprawidłowy zakres przy tworzeniu wycinka. Wyjście poza granice powoduje wyjątek w czasie wykonywania. Zawsze sprawdzaj długość i granice przed formowaniem wycinka.
Zapomnienie, że span odzwierciedla oryginalne dane. Każda zmiana przez Span<T> zmienia oryginalną tablicę. Jeśli potrzebujesz niezależnej kopii — skopiuj dane jawnie.
GO TO FULL VERSION