public class Object
{
...
protected virtual void Finalize() {}
}
За счет переопределения метода Finalize() в специальных классах устанавливается специфическое место для выполнения любой логики очистки, необходимой данному типу. Учитывая, что метод Finalize() определен как защищенный, вызывать его напрямую из экземпляра класса через операцию точки нельзя. Взамен метод Finalize(), если он поддерживается, будет вызываться сборщиком мусора перед удалением объекта из памяти.
На заметку! Переопределять метод Finalize() в типах структур не разрешено. Подобное ограничение вполне логично, поскольку структуры являются типами значений, которые изначально никогда не размещаются в куче и, следовательно, никогда не подвергаются сборке мусора. Тем не менее, при создании структуры, которая содержит неуправляемые ресурсы, нуждающиеся в очистке, можно реализовать интерфейс iDisposable (вскоре он будет описан). Вспомните из главы 4, что структуры ref и структуры ref, допускающие только чтение, не могут реализовывать какой-либо интерфейс, но могут реализовывать метод Dispose().
Разумеется, вызов метода Finalize() будет происходить (в итоге) во время "естественной" сборки мусора или в случае ее принудительного запуска внутри кода с помощью GC.Collect(). В предшествующих версиях .NET (но не в .NET Core) финализатор каждого объекта вызывался при окончании работы приложения. В .NET Core нет никаких способов принудительного запуска финализатора даже при завершении приложения.
О чем бы ни говорили ваши инстинкты разработчика, подавляющее большинство классов C# не требует написания явной логики очистки или специального финализатора. Причина проста: если в классах используются лишь другие управляемые объекты, то все они в конечном итоге будут подвергнуты сборке мусора. Единственная ситуация, когда может возникнуть потребность спроектировать класс, способный выполнять после себя очистку, предусматривает работу с неуправляемыми ресурсами (такими как низкоуровневые файловые дескрипторы операционной системы, низкоуровневые неуправляемые подключения к базам данных, фрагменты неуправляемой памяти и т.д.).
В рамках платформы .NET Core неуправляемые ресурсы получаются путем прямого обращения к API-интерфейсу операционной системы с применением служб вызова платформы (Platform Invocation Services — P/Invoke) или в сложных сценариях взаимодействия с СОМ. С учетом сказанного можно сформулировать еще одно правило сборки мусора.
Правило. Единственная серьезная причина для переопределения метода Finalize() связана с использованием в классе C# неуправляемых ресурсов через P/Invoke или сложные задачи взаимодействия с СОМ (обычно посредством разнообразных членов типа System.Runtime.InteropServices.Marshal). Это объясняется тем, что в таких сценариях производится манипулирование памятью, которой исполняющая среда управлять не может.
Переопределение метода System.Object.Finalize()
В том редком случае, когда строится класс С#, в котором применяются неуправляемые ресурсы, вы вполне очевидно захотите обеспечить предсказуемое освобождение занимаемой памяти. В качестве примера создадим новый проект консольного приложения C# по имени SimpleFinalize и вставим в него класс MyResourceWrapper, в котором используется неуправляемый ресурс (каким бы он ни был). Теперь необходимо переопределить метод Finalize(). Как ни странно, для этого нельзя применять ключевое слово override языка С#:
(window.adrunTag = window.adrunTag || []).push({v: 1, el: 'adrun-4-390', c: 4, b: 390})
using System;
namespace SimpleFinalize
{
class MyResourceWrapper
{
// Compile-time error!
protected override void Finalize(){ }
}
}
На самом деле для обеспечения того же самого эффекта используется синтаксис деструктора (подобный C++). Причина такой альтернативной формы переопределения виртуального метода заключается в том, что при обработке синтаксиса финализатора компилятор автоматически добавляет внутрь неявно переопределяемого метода Finalize() много обязательных инфраструктурных элементов (как вскоре будет показано).
Финализаторы C# выглядят похожими на конструкторы тем, что именуются идентично классу, в котором определены. Вдобавок они снабжаются префиксом в виде тильды (~). Однако в отличие от конструкторов финализаторы никогда не получают модификатор доступа (они всегда неявно защищенные), не принимают параметров и не могут быть перегружены (в каждом классе допускается наличие только одного финализатора). Ниже приведен специальный финализатор для класса MyResourceWrapper, который при вызове выдает звуковой сигнал. Очевидно, такой пример предназначен только для демонстрационных целей. В реальном приложении финализатор только освобождает любые неуправляемые ресурсы и не взаимодействует с другими управляемыми объектами, даже с теми, на которые ссылается текущий объект, т.к. нельзя предполагать, что они все еще существуют на момент вызова этого метода Finalize() сборщиком мусора.
using System;
// Переопределить System.Object.Finalize()
// посредством синтаксиса финализатора.
class MyResourceWrapper
{
// Очистить неуправляемые ресурсы.
// Выдать звуковой сигнал при уничтожении
// (только в целях тестирования)
~MyResourceWrapper() => Console.Beep();
}
Если теперь просмотреть код CIL данного финализатора с помощью утилиты ildasm.exe, то обнаружится, что компилятор добавил необходимый код для проверки ошибок. Первым делом операторы внутри области действия метода Finalize() помещены в блок try (см. главу 7). Связанный с ним блок finally гарантирует, что методы Finalize() базовых классов будут всегда выполняться независимо от любых исключений, возникших в области try.