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> для большей безопасности.

2
Задача
C# SELF, 65 уровень, 4 лекция
Недоступна
Асинхронная работа с Memory<T>
Асинхронная работа с Memory<T>
1
Опрос
Память в C#, 65 уровень, 4 лекция
Недоступен
Память в C#
Устройство памяти в .NET
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ