Продолжаем рубрику "Интересно о 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);
}
}
Более подробно нюансы реализации и использования финализаторов и деструкторов можно здесь и здесь.
6