1. Введение
Всё началось с того, что разработчики .NET хотели ускорить работу с большими объёмами данных и дать разработчикам возможность делать это максимально удобно и безопасно. Сначала появился Span<T>, который — это "окно" в непрерывный участок памяти, способный показывать часть массива, строки или даже память, выделенную вне .NET (например, через P/Invoke).
Однако у Span<T> есть одно мощное ограничение: он всегда должен жить только на стеке. Нельзя хранить его в полях классов, нельзя возвращать из методов, нельзя передавать между асинхронными методами. Причина — безопасность: если кто-то оставит ссылку на память, которой уже не существует, это гарантированный крах приложения.
Иногда нужно возвращать слайсы данных из методов, хранить их в коллекциях или полях классов, а также использовать в асинхронных API. Вот тут на сцену и выходит Memory<T> — по сути, это безопасная "долгоживущая" версия Span<T>, которая может жить в куче, передаваться между потоками, "жить" в свойствах и объектах и вести себя как порядочный объект .NET.
Есть и ещё один "старший брат" — ReadOnlyMemory<T>, который, как несложно догадаться, не даёт изменять лежащие под ним данные, зато позволяет читать их везде, где нужно.
2. В чём отличие между 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> для большей безопасности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ