Что такое деструктор (финализатор) в C# и когда его используют?

«Что такое деструктор (финализатор) в C# и когда его используют?» — вопрос из категории Управление памятью, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Деструктор (в терминологии C# — финализатор) — это специальный метод класса, который автоматически вызывается сборщиком мусора (GC) перед освобождением памяти, занятой объектом. Его основное и единственное разумное назначение — аварийное освобождение неуправляемых ресурсов, если разработчик забыл вызвать Dispose().

Синтаксис и важные ограничения:

class ResourceHolder
{
    // Неуправляемый ресурс (условно, например, дескриптор файла из WinAPI).
    private IntPtr _nativeHandle;

    // ФИНАЛИЗАТОР (синтаксически похож на деструктор в C++).
    ~ResourceHolder()
    {
        // ВАЖНО: Этот код выполнится в отдельном потоке финализации GC.
        // Время вызова НЕПРЕДСКАЗУЕМО (когда GC соберёт объект).
        if (_nativeHandle != IntPtr.Zero)
        {
            // Вызов нативной функции для освобождения ресурса.
            NativeMethods.CloseHandle(_nativeHandle);
            _nativeHandle = IntPtr.Zero;
        }
    }
}

Почему финализаторы — это крайняя мера?

Проблема Следствие
Недетерминированность: Вызов зависит от GC. Ресурс (файл, сокет) может оставаться занятым намного дольше необходимого.
Производительность: Объект с финализатором проходит более сложный цикл сборки (попадает в очередь финализации). Давление на GC, более долгая пауза.
Нет гарантии порядка: Нельзя полагаться на финализацию других объектов. Попытка освободить ресурс, который уже мог быть собран.

Правильный шаблон: IDisposable

Для детерминированного освобождения ресурсов всегда реализуйте интерфейс IDisposable.

class ProperResourceHolder : IDisposable
{
    private IntPtr _nativeHandle;
    private bool _disposed = false;

    // Публичный метод Dispose для явного освобождения.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // Сообщаем GC, что финализация не нужна.
    }

    // Защищённая виртуальная логика освобождения.
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Освобождаем управляемые ресурсы (другие IDisposable-объекты).
            }
            // Освобождаем неуправляемые ресурсы.
            if (_nativeHandle != IntPtr.Zero)
            {
                NativeMethods.CloseHandle(_nativeHandle);
                _nativeHandle = IntPtr.Zero;
            }
            _disposed = true;
        }
    }

    // ФИНАЛИЗАТОР — только как страховка на случай, если Dispose не вызвали.
    ~ProperResourceHolder()
    {
        Dispose(false);
    }
}

// Использование:
using (var holder = new ProperResourceHolder())
{
    // Работа с ресурсом.
} // Dispose() вызовется автоматически здесь, ресурс освободится немедленно.

Вывод: Используйте финализатор только как резервный механизм для освобождения неуправляемых ресурсов. Основной способ — вызов Dispose() через using или вручную.