Методы завершения (руководство по программированию в C#)
Методы завершения (также называемые деструкторами) используются для любой необходимой окончательной очистки, когда сборщик мусора окончательно удаляет экземпляра класса. В большинстве случаев можно избежать написания метода завершения с помощью System.Runtime.InteropServices.SafeHandle или производных классов для упаковки любого неуправляемого дескриптора.
Замечания
- В структурах определение методов завершения невозможно. Они применяются только в классах.
- Каждый класс может иметь только один метод завершения.
- Методы завершения не могут быть унаследованы или перегружены.
- Методы завершения невозможно вызвать. Они запускаются автоматически.
- Метод завершения не принимает модификаторов и не имеет параметров.
Например, ниже показано объявление метода завершения для класса Car
.
class Car
{
~Car() // finalizer
{
// cleanup statements...
}
}
Метод завершения можно также реализовать как определение тела выражения, как показано в следующем примере.
public class Destroyer
{
public override string ToString() => GetType().Name;
~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");
}
Метод завершения неявно вызывает метод Finalize для базового класса объекта. В связи с этим вызов метода завершения неявно преобразуется в следующий код:
protected override void Finalize()
{
try
{
// Cleanup statements...
}
finally
{
base.Finalize();
}
}
Это означает, что метод Finalize
вызывается рекурсивно для всех экземпляров цепочки наследования начиная с самого дальнего и заканчивая самым первым.
Примечание.
Пустые методы завершения использовать нельзя. Если класс содержит метод завершения, то в очереди метода Finalize
создается запись. Эта очередь обрабатывается сборщиком мусора. Когда сборщик мусора обрабатывает очередь, он вызывает каждый метод завершения. Ненужные методы завершения, включая пустые методы завершения, методы завершения, которые вызывают только средство завершения базового класса или методы завершения, которые вызывают только условные методы, вызывают ненужную потерю производительности.
Программист не может управлять моментом вызова метода завершения, поскольку это определяется сборщиком мусора. Сборщик мусора проверяет наличие объектов, которые больше не используются приложением. Если он считает, что какой-либо объект требует уничтожения, то вызывает метод завершения (при наличии) и освобождает память, используемую для хранения этого объекта. Сборку мусора можно выполнить принудительно, вызвав метод Collect, но в большинстве случаев этого следует избегать из-за возможных проблем с производительностью.
Примечание.
От конкретной реализации .NET зависит, выполняются ли методы завершения как часть завершения работы приложения. При завершении работы приложения .NET Framework прилагает все возможные усилия для вызова методов завершения для всех своих объектов, которые еще не были собраны сборщиком мусора, если такая очистка не была подавлена (например, вызовом метода библиотеки GC.SuppressFinalize
). .NET 5 (включая .NET Core) и более поздние версии не вызывают методы завершения как часть завершения приложения. Дополнительные сведения об этой проблеме см. в Проблема dotnet/csharpstandard #291, рассмотренная на сайте GitHub.
Если необходимо надежно выполнить очистку при наличии приложения, зарегистрируйте обработчик для события System.AppDomain.ProcessExit. Этот обработчик обеспечит вызов IDisposable.Dispose() (или, IAsyncDisposable.DisposeAsync()) для всех объектов, для которых требуется очистка перед выходом из приложения. Поскольку нельзя вызвать метод завершения напрямую и нельзя гарантировать, что сборщик мусора вызовет все методы завершения до выхода, необходимо использовать Dispose
или DisposeAsync
, чтобы освободить ресурсы.
Использование методов завершения для освобождения ресурсов
В целом язык C# не требует управления памятью в той степени, в какой это требуется в случае разработки кода на языке, не рассчитанном на среду выполнения со сборкой мусора. Это связано с тем, что сборщик мусора платформы .NET неявным образом управляет выделением и высвобождением памяти для объектов. Однако при инкапсуляции приложением неуправляемых ресурсов, например окон, файлов и сетевых подключений, для высвобождения этих ресурсов следует использовать методы завершения. Если объект допускает завершение, то сборщик мусора выполняет метод Finalize
этого объекта.
Освобождение ресурсов явным образом
В случае, когда приложением используется ценный внешний ресурс, также рекомендуется обеспечить способ высвобождения этого ресурса явным образом, прежде чем сборщик мусора освободит объект. Для высвобождения ресурса реализуется метод Dispose
интерфейса IDisposable, который выполняет необходимую для объекта очистку. Это может значительно повысить производительность приложения. Даже в случае использования такого явного управления ресурсами метод завершения становится резервным средством очистки ресурсов, если вызов метода Dispose
выполнить не удастся.
Дополнительные сведения об очистке ресурсов см. в следующих статьях:
- Очистка неуправляемых ресурсов
- Реализация метода dispose
- Реализация метода DisposeAsync
- Инструкция
using
Пример
В приведенном ниже примере создаются три класса, образующих цепочку наследования. Класс First
является базовым, класс Second
является производным от класса First
, а класс Third
является производным от класса Second
. Все три класса имеют методы завершения. В методе Main
создается экземпляр самого дальнего в цепочке наследования класса. Выходные данные этого кода зависят от того, на какую реализацию .NET нацелено приложение:
- .NET Framework: выходные данные показывают, что методы завершения для трех классов вызываются автоматически при завершении работы приложения, в порядке от наиболее производного до наименее производного.
- .NET 5 (включая .NET Core) или более поздняя версия: нет выходных данных, так как эта реализация .NET не вызывает методы завершения при завершении работы приложения.
class First
{
~First()
{
System.Diagnostics.Trace.WriteLine("First's finalizer is called.");
}
}
class Second : First
{
~Second()
{
System.Diagnostics.Trace.WriteLine("Second's finalizer is called.");
}
}
class Third : Second
{
~Third()
{
System.Diagnostics.Trace.WriteLine("Third's finalizer is called.");
}
}
/*
Test with code like the following:
Third t = new Third();
t = null;
When objects are finalized, the output would be:
Third's finalizer is called.
Second's finalizer is called.
First's finalizer is called.
*/
Спецификация языка C#
Дополнительные сведения см. в разделе "Методы завершения" спецификации языка C#.