Финализируемые объекты

95

В самом главном базовом классе .NET — System.Object — имеется виртуальный метод по имени Finalize(). В предлагаемой по умолчанию реализации он ничего особенного не делает:

// Класс System.Object
public class Object
{
  protected virtual void Finalize () {}
}

За счет его переопределения в специальных классах устанавливается специфическое место для выполнения любой необходимой данному типу логики по очистке. Из-за того, что метод Finalize() по определению является защищенным (protected), вызывать его напрямую из класса экземпляра с помощью операции точки не допускается. Вместо этого метод Finalize() (если он поддерживается) будет автоматически вызываться сборщиком мусора перед удалением соответствующего объекта из памяти.

Переопределять метод Finalize() в типах структур нельзя. Это вполне логичное ограничение, поскольку структуры представляют собой типы значения, которые изначально никогда не размещаются в управляемой памяти и, следовательно, никогда не подвергаются процессу сборки мусора. Однако в случае создания структуры, которая содержит ресурсы, нуждающиеся в очистке, вместо этого метода можно реализовать интерфейс IDisposable.

Разумеется, вызов метода Finalize() будет происходить (в конечном итоге) либо во время естественной активизации процесса сборки мусора, либо во время его принудительной активизации программным образом с помощью GC.Collect(). Помимо этого, финализатор типа будет автоматически вызываться и при выгрузке из памяти домена, который отвечает за обслуживание приложения. Некоторым по опыту работы с .NET уже может быть известно, что домены приложений (AppDomain) применяются для обслуживания исполняемой сборки и любых необходимых внешних библиотек кода. Пока что необходимо обратить внимание лишь на то, что при выгрузке домена приложения из памяти CLR-среда будет автоматически вызывать финализаторы для каждого финализируемого объекта, который был создан во время существования AppDomain.

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

Внутри .NET неуправляемые ресурсы появляются в результате непосредственного вызова API-интерфейса операционной системы с помощью служб PInvoke (Platform Invocation Services — службы вызова платформы) или применения очень сложных сценариев взаимодействия с СОМ. Ознакомьтесь со следующим правилом сборки мусора:

Единственная причина переопределения Finalize() связана с использованием в классе C# каких-то неуправляемых ресурсов через PInvoke или сложных процедур взаимодействия с СОМ (обычно посредством членов типа System.Runtime.InteropServices.Marshal).

Переопределение System.Object.Finalize()

В тех редких случаях, когда создается класс C#, в котором используются неуправляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение соответствующей памяти. Для этого необходимо переопределить метод Finalize(). Как ни странно, применять для этого ключевое слово override в C# не допускается.

Вместо этого для достижения того же эффекта должен применяться синтаксис деструктора (подобно С++). Объясняется это тем, что при обработке синтаксиса финализатора компилятор автоматически добавляет в неявно переопределяемый метод Finalize() приличное количество требуемых элементов инфраструктуры.

Давайте рассмотрим пример:

using System;

namespace ConsoleApplication1
{
    class FinalizeObject
    {
        public int id { get; set; }

        public FinalizeObject(int id)
        {
            this.id = id;
        }

        // Создадим специальный деструктор
        ~FinalizeObject()
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Объект №{0} уничтожен",id);
            Console.Beep();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.Read();
            // После того как будет нажата клавиша Enter (выход из программы)
            // все последующие объекты будут уничтожены

            FinalizeObject[] obj = new FinalizeObject[100];
            for (int i = 0; i < 100; i++)
                obj[i] = new FinalizeObject(i);
        }
    }
}

В данном примере, после завершения приложения (в данном случае консольного приложения при нажатия клавиши Enter) будет активирована сборка мусора которая удалит оставшиеся 100 объектов класса FinalizeObject, при этом запустится деструктор:

Финализация нескольких объектов

Описание процесса финализации

Чтобы не делать лишнюю работу, следует всегда помнить, что задачей метода Finalize() является забота о том, чтобы объект .NET мог освобождать неуправляемые ресурсы во время сборки мусора. Следовательно, при создании типа, в котором никакие неуправляемые сущности не используются (так бывает чаще всего), от финализации оказывается мало толку. На самом деле, всегда, когда возможно, следует стараться проектировать типы так, чтобы в них не поддерживался метод Finalize() по той очень простой причине, что выполнение финализации отнимает время.

При размещении объекта в управляемой куче исполняющая среда автоматически определяет, поддерживается ли в нем какой-нибудь специальный метод Finalize(). Если да, тогда она помечает его как финализируемый (finalizable) и сохраняет указатель на него во внутренней очереди, называемой очередью финализации (finalization queue). Эта очередь финализации представляет собой просматриваемую сборщиком мусора таблицу, где перечислены объекты, которые перед удалением из кучи должны быть обязательно финализированы.

Когда сборщик мусора определяет, что наступило время удалить объект из памяти, он проверяет каждую запись в очереди финализации и копирует объект из кучи в еще одну управляемую структуру, называемую таблицей объектов, доступных для финализации (finalization reachable table). После этого он создает отдельный поток для вызова метода Finalize() в отношении каждого из упоминаемых в этой таблице объектов при следующей сборке мусора. В результате получается, что для окончательной финализации объекта требуется как минимум два процесса сборки мусора.

Из всего вышесказанного следует, что хотя финализация объекта действительно позволяет гарантировать способность объекта освобождать неуправляемые ресурсы, она все равно остается недетерминированной по своей природе, и по причине дополнительной выполняемой незаметным образом обработки протекает гораздо медленнее.

Пройди тесты
Лучший чат для C# программистов