🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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ć awaitMemory<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.

SytuacjaUżyj
Parametr metody synchronicznej, dane używane od razuSpan<T>
Metoda async, dane potrzebne po awaitMemory<T>
Pole klasy przechowujące fragment danychMemory<T>
Przekazanie danych do lambdy/domknięciaMemory<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.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

Dodaj komentarz

czytanie to początek

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ę →