JavaRush /Курси /C# SELF /Memory<T> і <...

Memory<T> і ReadOnlyMemory<T>

C# SELF
Рівень 65 , Лекція 4
Відкрита

1. Вступ

Усе почалося з того, що розробники .NET хотіли пришвидшити роботу з великими обсягами даних і дати розробникам можливість робити це максимально зручно та безпечно. Спочатку з’явився Span<T> — «вікно» у неперервну ділянку памʼяті, здатне показувати частину масиву, рядка або навіть памʼять, виділену поза .NET (наприклад, через P/Invoke).

Проте у Span<T> є одне серйозне обмеження: він завжди має жити лише на стеку. Його не можна зберігати в полях класів, не можна повертати з методів і не можна передавати між асинхронними методами. Причина — безпека: якщо хтось залишить посилання на памʼять, якої вже не існує, це гарантований крах застосунку.

Іноді потрібно повертати зрізи даних із методів, зберігати їх у колекціях або полях класів, а також використовувати в асинхронних API. Саме тут на сцену виходить Memory<T> — по суті, безпечна «довгоживуча» версія Span<T>, яка може зберігатися в купі, передаватися між потоками, бути у властивостях і полях та поводитися як типовий .NET-об’єкт.

Є ще один «старший брат» — ReadOnlyMemory<T>, який, як неважко здогадатися, не дає змінювати дані, на які він посилається, зате дозволяє читати їх там, де потрібно.

2. У чому різниця між Span<T> і Memory<T>

Ось невелика таблиця для наочного порівняння:

Span<T>
Memory<T>
Де живе Лише на стеку (stack only) На стеку й у купі (heap/stack)
Можна зберігати в полі ❌ Ні ✅ Так
Можна повертати з методу ❌ Ні ✅ Так
Асинхронні/await-методи ❌ Не можна ✅ Можна
Змінюваний ✅ Є ще ReadOnlySpan<T> ✅ Є ще ReadOnlyMemory<T>
Дозволяє робити зрізи даних ✅ Так ✅ Так

Якщо потрібно швидко пройтися даними всередині методу — беріть Span<T>. Якщо потрібно повернути результат назовні або покласти його в поле класу — використовуйте Memory<T>. А якщо дані лише для читання — беріть ReadOnlyMemory<T>.

3. Сигнатура та базова будова Memory<T>

Усе звично: Memory<T> — дженерік, тобто універсальний тип. Можна створити Memory<int>, Memory<byte>, Memory<char> і навіть Memory<MyType>. Усередині Memory<T> ховається посилання на масив, рядок або інше джерело даних, а також діапазон (з якого індексу та скільки елементів).

Щоб отримати з Memory<T> швидкий доступ для обробки, використовують його властивість Span — так ви одразу отримуєте Span<T>, який можна застосовувати всередині синхронного методу.

4. Як створювати Memory<T>: практика

Приклад 1. Створення з масиву

int[] numbers = { 1, 2, 3, 4, 5, 6 };
Memory<int> memory = new Memory<int>(numbers); // Увесь масив

// Можна взяти «зріз» — частину масиву
Memory<int> slice = memory.Slice(2, 3); // елементи 2, 3 і 4

Приклад 2. Створення з рядка (через Memory<char>)

string text = "Привіт, світ!";
Memory<char> charMemory = text.AsMemory(); // Увесь текст як пам’ять
Memory<char> subMemory = charMemory.Slice(7, 3); // з 7-го символу, 3 символи («світ»)

Приклад 3. Використання ReadOnlyMemory<T>

Так само, але захищено від змін:

int[] data = { 10, 20, 30, 40 };
ReadOnlyMemory<int> readOnly = data; // Не дасть змінити через цей об’єкт

5. Перетворення між Memory<T> і Span<T>

Працювати з Memory<T> так само «гнучко» і швидко, як зі Span<T>, напряму не вийде — адже він призначений для іншого. Але коли справді треба швидко обробити шматок памʼяті, можна отримати «миттєвий» Span<T> з памʼяті через властивість Span:

void ProcessData(Memory<int> memory)
{
    Span<int> span = memory.Span;
    for (int i = 0; i < span.Length; i++)
    {
        span[i] += 100;
    }
}

Зверніть увагу: Span<T> працює лише всередині методу. Якщо спробувати повернути його назовні, компілятор видасть помилку.

У ReadOnlyMemory<T> усе аналогічно, тільки ви отримаєте ReadOnlySpan<T>, який не дозволить змінювати дані:

void PrintData(ReadOnlyMemory<int> memory)
{
    ReadOnlySpan<int> roSpan = memory.Span;
    foreach (var item in roSpan)
        Console.WriteLine(item);
}

6. Використання в реальних завданнях

Асинхронна обробка даних

Саме тут для Memory<T> і настає час. Його можна використовувати в асинхронних методах! Наприклад, для асинхронного читання файлів:

using System.IO;
using System.Threading.Tasks;

public async Task ReadFileAsync(string path)
{
    byte[] buffer = new byte[4096];
    using var stream = File.OpenRead(path);
    int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
    // Тепер можна працювати із buffer
}

Тут AsMemory передає буфер прямо в асинхронний метод, і жодних проблем із областю видимості або руйнуванням памʼяті, як було б зі Span<T>.

Зберігання зрізів даних у властивостях і полях

Іноді потрібно зробити клас, який зберігає «шматочок» великого масиву для подальшої передачі:

class DataChunk
{
    public Memory<byte> Data { get; }

    public DataChunk(Memory<byte> data)
    {
        Data = data;
    }
}

7. Використання з колекціями, рядками та масивами

З масивами

Найчастіше:

byte[] bytes = { 1, 2, 3, 4, 5 };
Memory<byte> mem = bytes;          // Увесь масив
Memory<byte> part = mem.Slice(2);  // З третього елемента до кінця

З рядками

Через AsMemory():

string hello = "Hello, Memory!";
ReadOnlyMemory<char> mem = hello.AsMemory(6, 6); // "Memory"

З колекціями (наприклад, List<T>)

Безпосередньо створити Memory<T> з List<T> не можна. Можна лише через масив:

List<int> list = new List<int> { 1, 2, 3 };
Memory<int> mem = list.ToArray(); // Копія, а не посилання!

Будьте уважні: якщо хочете уникнути копіювання — зберігайте дані в масиві.

8. Типові помилки під час роботи з Memory<T>

Помилка № 1: спроба використати Span<T> замість Memory<T> у полях класу. Не можна зберігати Span<T> у полях класу, бо він прив’язаний до стеку. Компілятор видасть помилку. Використовуйте Memory<T> для зберігання в купі.

Помилка № 2: очікування копіювання даних під час зрізів. Memory<T> не копіює дані, а створює «вікно» в наявний масив. Якщо змінити дані через один Memory<T>, це позначиться на всіх інших, що посилаються на ту саму памʼять.

Помилка № 3: спроба створити Memory<T> з List<T> напряму. Memory<T> працює лише з масивами, адже List<T> може змінювати розташування даних у памʼяті. Перетворіть список на масив через ToArray().

Помилка № 4: ігнорування ReadOnlyMemory<T> для незмінних даних. Якщо дані не потрібно змінювати, використовуйте ReadOnlyMemory<T> замість Memory<T> для більшої безпеки.

1
Опитування
Пам'ять у C#, рівень 65, лекція 4
Недоступний
Пам'ять у C#
Влаштування пам'яті в .NET
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ