• Правило. Необходимость переопределения Finalize() может возникать только тогда, когда класс C# использует неуправляемые ресурсы посредством PInvoke или при решении сложных задач взаимодействия с COM-объектами (обычно с применением типа System.Runtime.InteropServices.Marshal).
Замечание. В главе 3 уже отмечалось, что не допускается переопредешть Finalize() с типами структуры. Теперь это совершенно ясно, поскольку структуры являются типами, характеризуемыми значениями, и они не размещаются в динамической памяти.
Переопределение System.Object.Finalize()
В тех редких случаях, когда воздается C#-класс, использующий неуправляемые ресурсы, нужно гарантировать, что соответствующая память будет обрабатываться прогнозируемым образом. Предположим, что вы создали класс MyResourceWrapper, использующий неуправляемый ресурс (каким бы этот ресурс ни был), и вы хотите переопределить Finalize(). Немного странно, но в C# вы не можете для этого использовать ключевое слово override.
public class MyResourceWrapper {
// Ошибка компиляции!
protected override void Finalize(){}
}
Чтобы в пользовательском типе класса переопределить метод Finalize(), вы должны использовать в C# другой синтаксис деструктора, напоминающий синтаксис деструктора в C++. Причина в том, что при обработке деструктора компилятором C# в метод Finalize() автоматически добавляются необходимые элементы инфраструктуры (что будет продемонстрировано чуть позже).
Вот пример пользовательского деструктора для MyResourceWrapper, который при вызове генерирует системный сигнал. Ясно, что это сделано исключительно для примера. Настоящий деструктор должен только освобождать неуправляемые ресурсы, а не взаимодействовать с членами других управляемых o6ъeктов, поскольку вы не можете гарантировать, что эти объекты будут существовать в тот момент, когда сборщик мусора вызовет ваш метод Finalize().
// Переопределение System.Object.Finalize() с использованием
// синтаксиса деструктора.
class MyResourceWrapper {
~MyResourceWrapper() {
// Освобождение неуправляемых ресурсов.
// Завершающий сигнал (только для примера!)
Console.Веер();
}
}
Если рассмотреть этот деструктор с помощью ildasm.exe, вы увидите, что компилятор добавляет программный код контроля ошибок. Программный код вашего метода Finаlize() помещается в рамки блока try. Это делается для выявления операторов, которые во время выполнения могут сгенерировать ошибку (что формально называется исключительной ситуацией или исключением). Соответствующий блок finally гарантирует, что метод Finalize() класса будет выполнен независимо от исключений, которые могут возникать в рамках try. Формальности структурированной обработки исключений будут рассмотрены в следующей главе, а пока что проанализируйте следующее CIL-представление деструктора для C#-класса MyResourceWrapper.
.method family hidebysig virtual instance void Finalize() cil managed {
// Code size 13 (0xd)
.maxstack 1
.try {
IL_0000: ldc.i4 0x4e20
IL_0005: ldc.i4 0x3e8
IL 000a: call void [mscorlib]System.Console::Beep(int32, int32)
IL_000f: nop
IL_0010: nop
IL_0011: leave.s IL_001b
} // end.try
finally {
IL_0013: ldarg.0
IL_0014: call instance void [mscorlib]System.Object::Finalize()
IL_0019: nop
IL_001a: endfinally
} // end handler
IL_001b: nop
IL_001c: ret
} // end of method MyResourceWrapper::Finalize
При тестировании типа MyResourceWrapper вы обнаружите, что завершение работы приложения сопровождается системным звуковым сигналом, поскольку среда CLR автоматически вызывает деструкторы объектов при освобождении доменов приложений.
static void Main(string[] args) {
Console.WriteLine("***** Забавы с деструкторами *****n");
Console.WriteLine("Нажмите клавишу ввода для завершения работы");
Console.WriteLine("и вызова Finalize() сборщиком мусора");
Console.WriteLine("для объектов, предусматривающих финализацию.");
Console.ReadLine();
MyResourceWrapper rw = new MyResourceWrapper();
}
Исходный код. Проект SimpleFinalize размещен в подкаталоге, соответствующем главе 5.
Детали процесса финализации
Чтобы не делать лишней работы, следует помнить о том, что целью метода Finalize() является гарантия освобождения неуправляемых ресурсов .NET-объекта при сборке мусора. Поэтому при cоздании типа, не использующего неуправляемые элементы (а такая ситуация оказывается вполне типичней), от финализации будет мало пользы. На самом деле, при разработке своих типов вы должны избегать использования метода Finalize() по той очень простой причине, что финализация требует времени.
При размещений объекта в управляемой динамической памяти среда выполнения автоматически определяет, поддерживает ли этот объект пользовательский метод Finalize(). Если указанный метод поддерживается, то объект обозначается, как требующий финализации, а указатель на этот объект сохраняется во внутренней очереди, которую называют очередью финализации. Очередь финализации представляет собой таблицу, поддерживаемую сборщиком мусора и содержащую все объекты, для которых перед удалением из динамической памяти требуется финализация.
Когда с точки зрения сборщика мусора приходит время удалить объект из памяти, проверяются элементы очереди финализации, и соответствующий объект копируется из динамической памяти в другую управляемую структуру, которую вызывают таблицей элементов, доступных для финализации. В этот момент создается отдельный поток, задачей которого является вызов метода Finalize() при следующей сборке мусора для каждого объекта из таблицы элементов, доступных для финализации. С учетом этого становится. ясно, что для окончательного уничтожения объекта потребуется как минимум две процедуры сборки мусора.
Таким образом, хотя финализация объекта и гарантирует, что объект сможет освободить неуправляемые ресурсы, по своей природе эта процедура оказывается недетерминированной и, в связи с происходящими "за кулисами" дополнительными процессами, достаточно медленной.
Создание объектов, предусматривающих освобождение ресурсов
Поскольку многие неуправляемые ресурсы являются столь "драгоценными", что их нужно освободить как можно быстрее, предлагается рассмотреть еще один подход, используемый для "уборки" ресурсов объекта. В качестве альтернативы переопределению Finalize() класс может реализовать интерфейс IDisposable, который определяет единственный метод, имеющий имя Dispose().
public interface IDisposable {
void Dispose();
}
Если вы не имеете опыта программирования интерфейсов, то в главе 7 вы найдете все необходимые подробности. В сущности, интерфейс представляет собой набор абстрактных членов, которые могут поддерживаться классом или структурой. Если вы реализуете поддержку интерфейса IDisposable, то предполагается, что после завершения работы с объектом пользователь объекта должен вручную вызвать Dispose() до того, как объектная ссылка "уйдет" из области видимости. При таком подходе ваши объекты смогут выполнить всю необходимую "уборку" неуправляемых ресурсов без размещения в очереди финализации и без ожидания сборщика мусора, запускающего программную логику финализации класса.
Замечание. Интерфейс IDisposable может поддерживаться и типами структуры, и типами класса (в отличие от переопределения Finalize(), которое годится только для типов класса).
Ниже показан обновленный класс MyResourceWrapper, который теперь реализует IDisposable вместо переопределения System.Object.Finalize ().
// Реализация IDisposable.
public class MyResourceWrapper: IDisposable {
// Пользователь объекта должен вызвать этот метод
// перед завершением работы с объектом.
public void Dispose() {
// Освобождение неуправляемых ресурсов.
// Освобождение других содержащихся объектов.
}
}
Обратите внимание на то, что метод Dispose() отвечает не только за освобождение неуправляемых ресурсов типа, но и за вызов Dispose() для всех других содержащихся в его распоряжении объектов, предполагающих освобождение ресурсов. В отличие от Finalize(), обращаться из метода Dispose() к другим управляемым объектам вполне безопасно. Причина в том. что сборщик мусора не имеет никакого представления об интерфейсе IDisposable и никогда не вызывает Dispose(). Поэтому, когда пользователь объекта вызывает указанный метод, объект все еще существует в управляемой динамической памяти и имеет доступ ко всем другим объектам, размещенным в динамической памяти. Логика вызова проста.