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> для більшої безпеки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ