Сейчас я покажу как можно создать массив, у которого можно менять размер, не используя для этого нативные возможности языка. Т.е. вместо Array.Resize и var array = new int[100] только прямой доступ к памяти через unsafe контекст, который поможет избежать копирования и выделения новой памяти.
Эта заметка носит академический характер. Вряд ли вы будете это использовать в реальном проекте.
Сначала начну с результатов. Чтобы лучше было понятно, ради чего всё это.
Этот пример показывает, что фактически мы работаем с хранилищем внутри экземпляра класса ArrayNoGarbage. Но делаем это через полноценный массив, который существует как отдельная сущность.
Реализация
Подготовка. Нужно разрешить unsafe контекст. И установить через NuGet пакет System.Runtime.CompilerServices.Unsafe если его ещё нет в проекте. Класс Unsafe содержит универсальные низкоуровневые функции для управления управляемыми и неуправляемыми указателями.
План действий. Выделить участок памяти (хранилище), в котором будет расположен массив и служебные данные. При запросе на изменение длины массива, будем вручную менять его длину. Создадим массив с null значением и подсунем ему наш участок памяти.
Создаём generic класс ArrayNoGarbage, конструктор будет принимать значение, указывающее ёмкость. В дальнейшем эту ёмкость будем увеличивать если она меньше запрашиваемой длины.
В качестве generic типов будем принимать только unmanaged типы.
Хранилищем будет generic массив. Это поможет упростить код чтобы избавить себя от ручного менеджмента через класс Marshal. А также поможет избежать ручного объявления служебных данных, таких как Object Header и Method Table Ptr. Часть этого хранилища будет зарезервирована под служебные данные для нашего динамического массива.
Ниже полный код класса ArrayNoGarbage с подробными комментариями.
using System;
using System.Runtime.CompilerServices;
//Компилятор ругается на то, что массив является null поэтому подавляем эти предупреждения.
#pragma warning disable CS8618
#pragma warning disable CS8601
public sealed unsafe class ArrayNoGarbage<T> where T : unmanaged
{
//Хранилище для служебных и пользовательских данных.
private T[] storage;
//Этот массив будет создаваться вручную и являться частью хранилища данных.
//Фактически он не занимает места в памяти.
private T[] array;
//Количество элементов хранилища выделенное под служебные данные.
private readonly int elementOffset;
//Размер служебных данных таких как Object Header, Method Table Ptr и длина массива.
private readonly int ptrOffset;
//Минимальная вместимость хранилища.
private const int MIN_CAPACITY = 64;
public ArrayNoGarbage(int capacity = MIN_CAPACITY)
{
//Инициализация хранилища с заданной ёмкостью.
storage = new T[capacity < MIN_CAPACITY ? MIN_CAPACITY : capacity];
//Ссылка на хранилище.
var storageObjPtr = (IntPtr*) Unsafe.AsPointer(ref storage);
//Ссылка на заголовк хранилища.
var storageHeaderPtr = (*storageObjPtr).ToPointer();
//Ссылка на первый элемент хранилища.
var storageElement0Ptr = Unsafe.AsPointer(ref storage[0]);
//Находим размер служебных данных.
ptrOffset = (int) ((((IntPtr) storageElement0Ptr).ToInt64() -
((IntPtr) storageHeaderPtr).ToInt64()) / sizeof(nint));
//Вычисляем в какое количество элементов хранилища поместятся служебные данные.
elementOffset = sizeof(nint) * ptrOffset <= sizeof(T)
? 1
: sizeof(nint) * ptrOffset / sizeof(T);
//Ссылка на первый элемент создаваемого массива.
//Unsafe.Add и Unsafe.Subtract вычисляют смещение в памяти относительно текущего.
var arrayElement0Ptr = Unsafe.Add<T>(storageElement0Ptr, elementOffset);
//Ссылка на заголовок массива.
var arrayHeaderPtr = (nint*) Unsafe.Subtract<nint>(arrayElement0Ptr, ptrOffset);
//Копируем служебные данные массива,
//на данном этапе они полностью совпадают с данными хранилища,
//а потом начинают жить своей жизнью.
for (var i = 0; i < ptrOffset; i++)
{
arrayHeaderPtr[i] = ((nint*) storageHeaderPtr)[i];
}
//Ссылка на участок памяти в котором хранится длина массива.
var lengthPtr = (int*) Unsafe.Subtract<nint>(arrayElement0Ptr, 1);
//Устаналиваем длину массива за вычетом служебных данных.
lengthPtr[0] = storage.Length - elementOffset;
//Ссылка на массив. Сейчас он равен null. И ссылается на IntPtr.Zero.
var arrayObjPtr = (nint*) Unsafe.AsPointer(ref array);
//Записываем новую ссылку на заголовок массива. Теперь массив больше не null.
arrayObjPtr[0] = (nint) arrayHeaderPtr;
}
public T[] GetArray(int length)
{
//Если запрашиваемая длина совпадает с длиной массива, то ничего не делаем.
if (length == array.Length) return array;
//Если запрашиваемая длина больше, чем емкость выделенная под пользовательские данные,
//то изменяем размер хранилища.
if (length > storage.Length - elementOffset)
{
Array.Resize(ref storage, length + elementOffset);
//Ссылка на первый элемент хранилища.
var storageElement0Ptr = Unsafe.AsPointer(ref storage[0]);
//Ссылка на первый элемент массива.
var arrayElement0Ptr = Unsafe.Add<T>(storageElement0Ptr, elementOffset);
//Ссылка на заголовок массива.
var arrayHeaderPtr = Unsafe.Subtract<nint>(arrayElement0Ptr, ptrOffset);
//Ссылка на массив.
var arrayObjPtr = (nint*) Unsafe.AsPointer(ref array);
//Размер хранилища был изменён,
//наш массив сейчас ссылается на старый участок памяти.
//Записываем новую ссылку на заголовок массива.
//Старый участок памяти будет очищен сборщиком мусора
//на него больше не ссылается ни хранилище, ни наш массив.
arrayObjPtr[0] = (nint) arrayHeaderPtr;
}
{
//Ссылка на первый элемент массива.
var arrayElement0Ptr = Unsafe.AsPointer(ref array[0]);
//Ссылка на участок памяти в котором хранится длина массива.
var lengthPtr = (int*) Unsafe.Subtract<IntPtr>(arrayElement0Ptr, 1);
//Устанавливаем новую длину массива.
lengthPtr[0] = length;
}
return array;
}
}
#pragma warning restore CS8618
#pragma warning restore CS8601
Я не проводил тестирования в 32 битной среде. Возможно, есть какие-то проблемы.
Заключение
На этом всё. С помощью нехитрых манипуляций с памятью мы создали свой массив внутри другого массива и полностью управляем его размером без нагрузки на сборщик мусора.