Что такое деструктор (финализатор) в 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 или вручную.

Ответ 18+ 🔞

А, ну вот, финализаторы в C#! Это ж такая тема, где каждый второй разработчик думает, что он умный, а на деле просто страховку от своей же криворукости делает. Слушай, сейчас разжую.

Представь себе: есть у тебя объект, который хватает какой-нибудь нативный ресурс — файловый дескриптор из винды, кусок неуправляемой памяти, хрен его знает что. И ты, такой красавчик, забываешь его вовремя отпустить. Чтобы твоя забывчивость не привела к утечке на всю систему, в C# есть такая штука — финализатор. По сути, это предсмертная записка объекта для сборщика мусора: «Братан, когда будешь меня стирать, не забудь вот эту ручку в нативном мире закрыть, а то там всё посыпется».

Выглядит он почти как деструктор в плюсах, но это чисто внешнее сходство, обманка для глаз.

class MyMess
{
    private IntPtr _nativeGadget; // Допустим, тут наша неуправляемая фигня

    // А вот и он, финализатор! Тильда и имя класса.
    ~MyMess()
    {
        // ВНИМАНИЕ! Этот код выполнится НЕИЗВЕСТНО КОГДА.
        // Когда GC протрезвеет и решит собрать объект.
        if (_nativeGadget != IntPtr.Zero)
        {
            SomeNativeLib.FreeThatShit(_nativeGadget); // Освобождаем
            _nativeGadget = IntPtr.Zero;
        }
    }
}

Вроде бы удобно, да? Написал и забыл. Ан нет! Финализатор — это не способ работы, это способ отчаяния. Почему? Да потому что он нихрена не детерминированный!

Проблемы, из-за которых финализаторы — это пиздец:

  1. Непредсказуемость. Твой объект с финализатором может болтаться в памяти до второго пришествия, пока GC не соизволит его собрать. А файл или сокет, который он держит, будет занят. Представь, что ты закрыл программу, а она ещё полчаса в фоне ресурсы не отпускает. Красота!
  2. Производительность. Объект с финализатором — это головная боль для GC. Его нельзя просто взять и удалить. Сначала он попадает в специальную очередь финализации, потом для него вызывается этот самый ~MyMess(), и только потом память можно по-честному освобождать. Всё это время он живёт и кушает память. Создал ты таких объектов овердохуища — и привет, лаги и просадки.
  3. Порядка нет. Нельзя рассчитывать, что финализатор одного объекта вызовется раньше финализатора другого. Можешь попытаться освободить ресурс, который уже сам по себе был собран. Полный разброд и шатание.

Так что же делать, если ресурсы надо освобождать? А делать надо по-взрослому, через IDisposable.

Вот смотри, как это выглядит у нормальных людей:

class ProperClass : IDisposable
{
    private IntPtr _nativeGadget;
    private bool _disposed = false; // Флажок, чтобы дважды не освобождать

    // Основной метод, который ВСЕ должны вызывать
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // Говорим GC: «Не парься с финализатором, я уже всё сделал».
    }

    // Сердцевина всей логики
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Здесь освобождаем УПРАВЛЯЕМЫЕ ресурсы (другие IDisposable-объекты).
                // _someManagedStream?.Dispose();
            }

            // А здесь освобождаем НЕУПРАВЛЯЕМЫЕ ресурсы.
            if (_nativeGadget != IntPtr.Zero)
            {
                SomeNativeLib.FreeThatShit(_nativeGadget);
                _nativeGadget = IntPtr.Zero;
            }

            _disposed = true;
        }
    }

    // ФИНАЛИЗАТОР — только как подстраховка, если кто-то забыл вызвать Dispose!
    ~ProperClass()
    {
        Dispose(false); // Вызываем с false, потому что управляемые ресурсы уже могли быть собраны
    }
}

А использовать эту красоту — одно удовольствие:

// Идеальный вариант — using. Dispose() вызовется сам в конце блока.
using (var resource = new ProperClass())
{
    // Работаем...
} // Тут ресурс освобождается МГНОВЕННО, а не когда GC захочет.

// Либо вызываем Dispose() вручную в try-finally.

Итог, блядь: Финализатор — это как аварийный молоток в стекле. Надеешься, что никогда не пригодится, но на всякий пожарный он есть. Основная работа — за Dispose(). Пишешь финализатор только для того, чтобы подстраховаться от утечки НЕУПРАВЛЯЕМЫХ ресурсов, если какой-то мудак (возможно, ты сам через полгода) забудет вызвать Dispose(). А если у тебя только управляемые ресурсы — можешь про финализаторы вообще забыть, как про страшный сон.