Что нужно сделать, чтобы создать класс, использующий неуправляемые ресурсы в .NET
Что проверяют
Понимаете ли вы, что сборщик мусора не знает про память, выделенную вне рантайма, и умеете ли написать освобождение так, чтобы не навредить финализатором.
Как отвечать
Начать с современного варианта: сырой дескриптор заворачивают в наследника SafeHandle. Он сам финализируется, корректно ведёт себя при выгрузке домена и не даёт освободить хендл, пока его использует вызов. Тогда классу остаётся реализовать IDisposable и освободить поле, а свой финализатор не нужен.
Полный шаблон нужен, когда ресурс держат напрямую:
public sealed class Native : IDisposable
{
private IntPtr handle;
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // финализатор уже не нужен
}
private void Dispose(bool disposing)
{
if (disposed) return;
if (disposing) { /* освобождаем управляемые поля */ }
if (handle != IntPtr.Zero) { Free(handle); handle = IntPtr.Zero; }
disposed = true;
}
~Native() => Dispose(false);
}Смысл флага: при вызове из финализатора трогать другие управляемые объекты нельзя, они могли быть собраны раньше. Вызывающий код пишет using var n = new Native();, а если освобождение асинхронное, класс дополняют интерфейсом IAsyncDisposable.
Где спотыкаются
Добавляют финализатор на всякий случай. Объект с финализатором переживает лишнюю сборку, уезжает в старшее поколение и освобождается позже. Если неуправляемых полей нет, а есть только другие IDisposable, финализатор только вредит.
Освобождают управляемые объекты в ветке финализатора: пишут в лог, обращаются к соседним полям. Порядок финализации не определён, объект мог быть собран раньше, и получаются редкие падения при завершении процесса.
Ломают наследование и повторный вызов. Наследник переопределяет Dispose(bool) и не зовёт базовую реализацию, ресурс базового класса утекает. Ещё забывают, что повторный Dispose() обязан быть безопасным, а работа с объектом после освобождения должна давать ObjectDisposedException, а не падение в нативном коде.