Memory w C# — bezalokacyjne bufory w metodach async i polach klas
Jeśli czytałeś o Span<T> w C#, znasz już problem: kopiowanie danych przy każdym Substring() czy wycinku tablicy kosztuje pamięć i czas GC. Span<T> to rozwiązuje — ale ma jedno poważne ograniczenie: jako ref struct nie może żyć w polu klasy ani przetrwać await. Memory<T> istnieje właśnie po to, żeby dać te same korzyści tam, gdzie Span<T> się nie skompiluje.
Problem: Span<T> nie przetrwa await
// ❌ Błąd kompilacji — Span<T> jako ref struct nie może przekroczyć await
public async Task<int> WczytajAsync(Stream stream, Span<byte> buffer)
{
return await stream.ReadAsync(buffer); // nie skompiluje się
}
Kompilator nie pozwoli na to celowo: Span<T> może wskazywać na pamięć stosu, a await może wznowić metodę na zupełnie innym wątku, z innym stosem. Gdyby to zadziałało, mógłbyś odczytać już nieistniejącą pamięć.
Czym jest Memory<T>
Memory<T> to zwykły struct (nie ref struct) — widok na ciągły fragment pamięci, tak jak Span<T>, ale bez jego ograniczeń. Można go trzymać w polu klasy, przekazywać przez async/await, przechwytywać w lambdzie. Cena: żeby faktycznie odczytać lub zapisać dane, trzeba najpierw zamienić go na Span<T> przez właściwość .Span.
byte[] tablica = new byte[1024];
Memory<byte> memory = tablica; // widok na całą tablicę, zero kopiowania
Span<byte> span = memory.Span; // konwersja "na żądanie", tania i synchroniczna
span[0] = 42;
Console.WriteLine(tablica[0]); // 42 — to wciąż ta sama pamięć
Memory<T> w praktyce — odczyt strumienia bez alokacji w pętli
Klasyczny problem: odczytujesz dane ze strumienia (plik, sieć) w pętli i za każdym razem alokujesz nową tablicę na bufor. Memory<T> pozwala użyć jednego bufora wielokrotnie:
byte[] buffer = new byte[4096];
Memory<byte> memory = buffer;
int bytesRead;
while ((bytesRead = await stream.ReadAsync(memory)) > 0)
{
ReadOnlySpan<byte> odebrane = memory.Span[..bytesRead];
Przetworz(odebrane); // operujesz na fragmencie tego samego bufora za każdym razem
}
Ani jedna alokacja w pętli — buffer jest tworzony raz, memory to tylko widok na niego, a ReadAsync bezpiecznie przechodzi przez await, bo Memory<T> nie ma ograniczeń ref struct.
Memory<T> jako pole klasy — czego Span<T> nie potrafi
public class Parser
{
private readonly ReadOnlyMemory<char> _tekst; // ✅ OK — Memory może być polem
public Parser(string tekst) => _tekst = tekst.AsMemory();
public ReadOnlySpan<char> Wiersz(int start, int dlugosc)
=> _tekst.Span.Slice(start, dlugosc); // Span tylko lokalnie, w metodzie
}
Span<T> w tej samej roli w ogóle by się nie skompilował — próba zadeklarowania private Span<char> _tekst; w klasie kończy się błędem kompilatora. Memory<T> przechowuje referencję bezpiecznie między wywołaniami metod, a Span<T> pojawia się dopiero na chwilę, wewnątrz metody, gdy faktycznie potrzebujesz odczytać dane.
ReadOnlyMemory<T> dla stringów
Tak jak string.AsSpan() zwraca ReadOnlySpan<char> (bo stringi są niemutowalne), string.AsMemory() zwraca ReadOnlyMemory<char> — bezpieczny do przechowania w polu, przekazania przez async, bez kopiowania samego tekstu.
Kiedy Span<T>, a kiedy Memory<T>
Prosta zasada: Span<T> jako parametr metody synchronicznej, Memory<T> wszędzie tam, gdzie dane muszą przetrwać poza jednym, bezpośrednim wywołaniem — w polu klasy, w async/await, w kolejce zadań do wykonania później.
| Sytuacja | Użyj |
|---|---|
| Parametr metody synchronicznej, dane używane od razu | Span<T> |
Metoda async, dane potrzebne po await | Memory<T> |
| Pole klasy przechowujące fragment danych | Memory<T> |
| Przekazanie danych do lambdy/domknięcia | Memory<T> |
Nie zamieniaj wszystkiego na Memory<T> “na wszelki wypadek” — konwersja .Span przy każdym dostępie do danych ma niewielki, ale realny koszt. Tam, gdzie Span<T> się kompiluje, jest szybszy wybór.
Podsumowanie
Memory<T> to Span<T> bez ograniczeń ref struct — kosztem pośredniej konwersji przez .Span przy każdym faktycznym dostępie do danych. Używaj go tam, gdzie fragment danych musi przetrwać await, żyć w polu klasy albo zostać przechwycony przez lambdę — wszędzie indziej Span<T> wystarczy i jest szybszy.
🚀 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ę →