Span w C# — jak pisać kod bez zbędnych alokacji pamięci
Każde wywołanie Substring(), array.Skip(1).Take(3) czy string.Split() tworzy w pamięci nową kopię danych. W kodzie, który parsuje raz plik konfiguracyjny, to nieistotne. W kodzie, który parsuje tysiące wierszy na sekundę — te kopie zaczynają obciążać Garbage Collector i realnie spowalniają aplikację. Span<T> rozwiązuje ten problem: pozwala operować na fragmencie istniejących danych bez kopiowania ani jednego bajta.
Problem, który Span<T> rozwiązuje
string line = "imie=Anna;wiek=30";
int separatorIndex = line.IndexOf(';');
string firstPair = line.Substring(0, separatorIndex); // nowa alokacja w pamięci
Substring() wygląda niewinnie, ale za kulisami tworzy nowy obiekt string i kopiuje do niego znaki. Zrób to w pętli przetwarzającej duży plik CSV albo strumień danych z sieci, a Garbage Collector zacznie sprzątać po tobie w coraz krótszych odstępach.
Czym jest Span<T>
Span<T> to widok (ang. view) na ciągły fragment pamięci — tablicy, stringa, bloku pamięci ze stosu — bez kopiowania. To para: wskaźnik na początek danych plus długość. Odczyt i zapis przez Span<T> operuje bezpośrednio na oryginalnych danych.
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> slice = numbers.AsSpan(1, 3); // widok na { 2, 3, 4 } — zero kopiowania
slice[0] = 99;
Console.WriteLine(numbers[1]); // 99 — zmiana w slice zmienia oryginalną tablicę!
To ostatnia linijka jest kluczowa do zrozumienia: Span<T> nie jest kopią — to inne okno na te same dane.
Span<T> w praktyce — parsowanie bez alokacji
Ten sam parsing co wyżej, ale bez ani jednej alokacji stringa:
ReadOnlySpan<char> line = "imie=Anna;wiek=30".AsSpan();
int separatorIndex = line.IndexOf(';');
ReadOnlySpan<char> firstPair = line[..separatorIndex]; // "imie=Anna"
ReadOnlySpan<char> secondPair = line[(separatorIndex + 1)..]; // "wiek=30"
int equalsIndex = firstPair.IndexOf('=');
ReadOnlySpan<char> key = firstPair[..equalsIndex]; // "imie"
ReadOnlySpan<char> value = firstPair[(equalsIndex + 1)..]; // "Anna"
Console.WriteLine($"{key} -> {value}");
Żadna z operacji [..] (zakres) czy IndexOf nie tworzy nowego stringa w pamięci — wszystkie działają na tym samym buforze znaków co oryginalny line.
ReadOnlySpan<T> — dlaczego stringi dostają wersję “tylko do odczytu”
Stringi w C# są niemutowalne — nie da się zmienić pojedynczego znaku w istniejącym stringu. Dlatego "tekst".AsSpan() zwraca ReadOnlySpan<char>, a nie Span<char> — próba zapisu przez taki widok złamałaby fundamentalną gwarancję niemutowalności stringów. Dla tablic (int[], byte[]), które są mutowalne, AsSpan() zwraca zwykły, zapisywalny Span<T>.
Stackalloc — pamięć na stosie zamiast na stercie
Span<T> potrafi też wskazywać na pamięć zaalokowaną na stosie zamiast na stercie zarządzanej przez GC — przydatne dla małych, tymczasowych buforów:
Span<byte> buffer = stackalloc byte[256];
// buffer istnieje tylko w obrębie tej metody, GC nigdy o nim nie wie
Ograniczenia — dlaczego Span<T> nie zadziała wszędzie
Span<T> to ref struct — specjalny typ wartościowy z twardymi ograniczeniami wymuszanymi przez kompilator, właśnie po to, żeby mógł bezpiecznie wskazywać na pamięć stosu:
// ❌ Błąd kompilacji: ref struct nie może być polem klasy
public class Zly
{
private Span<int> _dane;
}
// ❌ Błąd kompilacji: ref struct nie może przetrwać poza async/await
public async Task PrzetworzAsync(Span<int> dane)
{
await Task.Delay(10);
}
// ❌ Błąd kompilacji: ref struct nie może być przechwycony przez lambdę/domknięcie
Span<int> dane = stackalloc int[10];
Action zla = () => Console.WriteLine(dane[0]);
Te ograniczenia nie są przypadkiem — kompilator pilnuje, żeby Span<T> nigdy nie “przeżył” dłużej niż dane, na które wskazuje. Gdyby to było możliwe, można by łatwo dostać się do już zwolnionej pamięci stosu.
Dla scenariuszy, gdzie potrzebujesz podobnej funkcjonalności w polu klasy albo w metodzie async — jest Memory<T>, który nie ma tych ograniczeń (kosztem odrobiny wydajności).
Kiedy używać Span<T>, a kiedy nie
Używaj Span<T>, gdy:
- parsujesz duże ilości tekstu lub danych binarnych (pliki, sieć, streaming),
- piszesz kod wykonywany w gorącej pętli (hot path), gdzie alokacje realnie wpływają na wydajność,
- potrzebujesz operować na fragmencie tablicy bez kopiowania.
Zostań przy zwykłych string/array, gdy:
- kod nie jest na ścieżce krytycznej wydajnościowo — czytelność wygrywa z mikrooptymalizacją,
- potrzebujesz przechować dane w polu klasy, użyć ich w
async/awaitalbo przekazać do lambdy — wtedy sięgnij poMemory<T>.
Podsumowanie
Span<T> to widok na istniejące dane w pamięci, a nie ich kopia — dzięki temu operacje takie jak parsowanie czy wycinanie fragmentów tablicy nie obciążają Garbage Collectora. Cena za to: Span<T> jako ref struct nie może żyć w polu klasy, metodzie async ani domknięciu lambdy. To narzędzie do konkretnego problemu (wydajność na ścieżce krytycznej), nie zamiennik string/array na co dzień.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
1 comment