Интересно о C#: какая разница между destructor и finalizer?
Продолжаем рубрику "Интересно о C#". После не шуточного обсуждения этого вопроса считаю нужным расставить все точки по данному вопросу.
И деструкторы и финалайзеры являются механизмом очищения ресурса после того, как он больше не используется.
Термин "destructor" чаще всего используется в значении детерминировано (т.е. последовательно) вызываемой очистки (deterministically-invoked cleanup), в то время как "finalizer" исполняется тогда, когда получает команду от сборщика мусора (garbadge collector).
Значит ли это, что спецификация по C# использует понятие деструктора неправильно?
Да, в такой постановке спецификация называет деструктором то, что является финалайзером, а то, что мы называем методом "Dispose()", который вызывается в конструкции using, по сути, является деструктором. CLI спецификация использует понятие "финалайзер" правильно.
Почему авторы C# спецификации используют неправильное значение?
Есть две гипотезы.Гипотеза #1. 12 мая 1999 года не было Википедии с описанием разницы между этими двумя понятиями. Возможно, это была простая ошибка по причине того, что эти два понятия имели одно и то же значение, а разница в значениях появилась позже для различения eager/deterministic и lazy/nondeterministic методов очистки.
Гипотеза #2. 12 мая 1999 года language design committee хотел оставить открытой возможность реализовывать C# "destructor" не в таком виде, как CLR finalizer. Т.е. "destructor" был разработан как концепт языка C#, который не обязательно должен являться точной копией концепта CLR "finalizer".
Комментарии комитета:
We're going to use the term "destructor" for the member which executes when an instance is reclaimed. Classes can have destructors; structs can't. Unlike in C++, a destructor cannot be called explicitly. Destruction is non-deterministic – you can't reliably know when the destructor will execute, except to say that it executes at some point after all references to the object have been released. The destructors in an inheritance chain are called in order, from most descendant to least descendant. There is no need (and no way) for the derived class to explicitly call the base destructor. The C# compiler compiles destructors to the appropriate CLR representation. For this version that probably means an instance finalizer that is distinguished in metadata.
... что косвенно подтверждает вторую гипотезу.
Ссылки:
Update.Интерфейс IDisposable имеет вид:
public interface IDisposable { void Dispose(); }
и соответствующий класс:
public class MyClass : IDisposable { public void Dispose() { // Perform any object clean up here. // If you are inheriting from another class that // also implements IDisposable, don't forget to // call base.Dispose() as well. } }
Неявная очистка (Implicit cleanup)
Неявная очистка должна быть реализована везде, где это возможно, путем защиты ресурсов с помощью SafeHandle. В .NET 1.1 этого класса не существовало, поэтому раньше необходимо было реализовывать финализатор. В последних версиях .NET необходимость реализовывать финализаторы практически отпала. Если такая необходимость все же есть, то можно реализовать protected Finalize метод используя синтаксис используемого языка. Исполняемая среда (runtime) вызовет метод Finalize как часть процесса финализации GC.
Не нужно реализовывать финализаторы кроме крайних случаев. Процесс их написания сложный и сделает использование вашего класса более дорогостоящим (expensive) даже если он ни разу не выполнится. Все объекты, которые реализуют финализаторы должны быть включены в список объектов финализации (finalization queue), который обслуживает GC. Поэтому если вам необходимо освободить ресурсы используйте Dispose метод, а не финализаторы. Если финализатор вам нужен, используйте его в дополнение методу Dispose, а не вместо него.
Синтаксические различия финализаторов и деструкторов:
Language | Destructor Syntax | Finalizer Syntax |
C# | public void Dispose() |
~T() |
C++ (.NET 2.0) | ~T() |
!T() |
C++ (.NET 1.0/1.1) | public void Dispose() |
~T() |
Visual Basic (.NET) | Public Sub Dispose() Implements IDisposable.Dispose |
Protected Overrides Sub Finalize() |
Dispose Pattern
Без использования финализатора:
public class SimpleCleanup : IDisposable { // some fields that require cleanup private SafeHandle handle; private bool disposed = false; // to detect redundant calls public SimpleCleanup() { this.handle = /*...*/; } protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { if (handle != null) { handle.Dispose(); } } disposed = true; } } public void Dispose() { Dispose(true); } }
C использованием финализатора:
public class ComplexCleanupBase : IDisposable { // some fields that require cleanup private bool disposed = false; // to detect redundant calls public ComplexCleanupBase() { // allocate resources } protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { // dispose-only, i.e. non-finalizable logic } // shared cleanup logic disposed = true; } } ~ComplexCleanupBase() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } public class ComplexCleanupExtender : ComplexCleanupBase { // some new fields that require cleanup private bool disposed = false; // to detect redundant calls public ComplexCleanupExtender() : base() { // allocate more resources (in addition to base’s) } protected override void Dispose(bool disposing) { if (!disposed) { if (disposing) { // dispose-only, i.e. non-finalizable logic } // new shared cleanup logic disposed = true; } base.Dispose(disposing); } }
Более подробно нюансы реализации и использования финализаторов и деструкторов можно здесь и здесь.