Давайте посмотрим, что об этом сказано в блоге о предстоящих изменениях в С# 8.0 (версия Visual Studio 2019 Preview 2):
«stack-only структуры появились в С# 7.2. Они чрезвычайно полезны, но при этом их использование тесно связано с ограничениями, например невозможностью реализовывать интерфейсы. Теперь ссылочные структуры можно очищать с помощью метода Dispose внутри них без использования интерфейса IDisposable».
Так и есть: stack-only ref структуры не реализуют интерфейсы, иначе возникала бы вероятность их упаковки. Следовательно, они не могут реализовывать IDisposable, и мы не можем использовать эти структуры в операторе using:
class Program
{
static void Main(string[] args)
{
using (var book = new Book())
{
Console.WriteLine("Hello World!");
}
}
}
ref struct Book : IDisposable
{
public void Dispose()
{
}
}
Попытка запустить этот код приведёт к ошибке компиляции:
Error CS8343 'Book': ref structs cannot implement interfaces
Однако теперь, если мы добавим публичный метод Dispose
к ссылочной структуре, оператор using
магическим образом примет её, и всё скомпилируется:
class Program
{
static void Main(string[] args)
{
using (var book = new Book())
{
// ...
}
}
}
ref struct Book
{
public void Dispose()
{
}
}
Более того, благодаря изменениям в самом операторе теперь можно использовать using в более краткой форме (так называемые объявления using
):
class Program
{
static void Main(string[] args)
{
using var book = new Book();
// ...
}
}
Но… зачем?
Это — длинная история, но в целом явная очистка (детерминированная финализация) предпочтительнее, чем неявная (недетерминированная финализация). Это понятно на интуитивном уровне. Лучше явно очистить ресурсы как можно скорее (вызвав Close, Dispose или оператор using), вместо того чтобы ждать неявной очистки, которая произойдёт «когда-нибудь» (когда сама среда запустит финализаторы).
Поэтому, при создании типа, владеющего неким ресурсом, лучше предусмотреть возможность очистки явным образом. В С# это очевидно интерфейс IDisposable
и его метод Dispose
.
Примечание. Не забывайте, что в случае ссылочных структур используется только явная очистка, поскольку определение финализаторов для них невозможно.
Давайте рассмотрим иллюстративный пример обычной «обёртки для пула неуправляемой памяти». Она занимает минимально возможное место (куча не используется совсем) именно благодаря ссылочной структуре, предназначенной для людей, помешанных на производительности:
public unsafe ref struct UnmanagedArray<T> where T : unmanaged
{
private T* data;
public UnmanagedArray(int length)
{
data = // get memory from some pool
}
public ref T this[int index]
{
get { return ref data[index]; }
}
public void Dispose()
{
// return memory to the pool
}
}
Поскольку в обёртку заключён неуправляемый ресурс, для очистки после использования мы применяем метод Dispose. Таким образом, пример выглядит как-то так:
static void Main(string[] args)
{
var array = new UnmanagedArray<int>(10);
Console.WriteLine(array[0]);
array.Dispose();
}
Это неудобно, поскольку нужно помнить о вызове Dispose. Кроме того, это болезненное решение, поскольку обработка исключений должным образом здесь неприменима. Поэтому, для того чтобы Dispose мог быть вызван изнутри, ввели оператор using. Однако ранее, как уже говорилось, применять его в этой ситуации было нельзя.
Но в С# 8.0 можно использовать преимущества оператора using по полной:
static void Main(string[] args)
{
using (var array = new UnmanagedArray<int>(10))
{
Console.WriteLine(array[0]);
}
}
При этом код стал лаконичнее благодаря объявлениям:
static void Main(string[] args)
{
using var array = new UnmanagedArray<int>(10);
Console.WriteLine(array[0]);
}
Два других примера внизу (значительная часть кода опущена для краткости) взяты из репозитория CoreFX.
Первый пример – ссылочная структура ValueUtf8Converter, которая оборачивает массив byte[] из пула массивов:
internal ref struct ValueUtf8Converter
{
private byte[] _arrayToReturnToPool;
...
public ValueUtf8Converter(Span<byte> initialBuffer)
{
_arrayToReturnToPool = null;
}
public Span<byte> ConvertAndTerminateString(ReadOnlySpan<char> value)
{
...
}
public void Dispose()
{
byte[] toReturn = _arrayToReturnToPool;
if (toReturn != null)
{
_arrayToReturnToPool = null;
ArrayPool<byte>.Shared.Return(toReturn);
}
}
}
Второй пример – RegexWriter, оборачивающий две ссылочные структуры ValueListBuilder, которые необходимо очистить явным образом (поскольку они тоже управляют массивами из пула массивов):
internal ref struct RegexWriter
{
...
private ValueListBuilder<int> _emitted;
private ValueListBuilder<int> _intStack;
...
public void Dispose()
{
_emitted.Dispose();
_intStack.Dispose();
}
}
Заключение
Удаляемые ссылочные структуры можно рассматривать как занимающие мало место типы, у которых есть РЕАЛЬНЫЙ деструктор, как в C++. Он будет задействован, как только соответствующий экземпляр выйдет за пределы области оператора using (или области видимости в случае объявления using).
Конечно, они не станут внезапно популярными при написании обычных, ориентированных на коммерческие цели программ, но, если вы создаёте высокопроизводительный, низкоуровневый код, о них стоит знать.
А ещё у нас есть статья про нашу конференцию:
Автор: sidristij