Dispose finalize что это за методы как используются в net
Перейти к содержимому

Dispose finalize что это за методы как используются в net

  • автор:

Различие Dispose , Finalize, Деструктора и класса GC

Выходит, что деструктор и финализатор это одно и то же ? А срабатывает ли конструкция using в объекте, где не реализовывают IDisposable ?

6 мар 2016 в 17:41

@PolyakovSergey русским же языком написал: нет, не одно и тоже. Даже написал, в чем разница. Читайте внимательнее.

6 мар 2016 в 17:42
GС руками вообще лучше не трогать, без крайней необходимости.
6 мар 2016 в 17:49

Следует еще помнить про то, что при явном определении метода Finalize, во время создания объекта в куче, CLR проверяет, содержет ли объект специальный метод Finalize и если да, то данный объект помечается как финализированный.

У CLR есть специальная внутренняя очередь – «очередь финацилизации», в которую кладется ссылка на финализируемый объект. По сути, на самом деле это просто таблица, которую сборщик мусора дергает, и получает по ней информацию по объектам, которые должны быть финализированы перед началом сборки мусора.

Если сборщик мусора планирует «прибрать» объект, который лежит в очереди финализации, сначала он копирует его в промежуточную таблицу финализации, а затем в отдельном потоке выполняет финализацию объектов из этой таблицы.

В результате процесс сборки состоит минимум из двух этапов, и содержит довольно большое кол-во действий, которые необходимо проделать, что будет сказываться на производительности.

Dispose finalize что это за методы как используются в net

Большинство объектов, используемых в программах на C#, относятся к управляемым или managed-коду. Такие объекты управляются CLR и легко очищаются сборщиком мусора. Однако вместе с тем встречаются также и такие объекты, которые задействуют неуправляемые объекты (подключения к файлам, базам данных, сетевые подключения и т.д.). Такие неуправляемые объекты обращаются к API операционной системы. Сборщик мусора может справиться с управляемыми объектами, однако он не знает, как удалять неуправляемые объекты. В этом случае разработчик должен сам реализовывать механизмы очистки на уровне программного кода.

Освобождение неуправляемых ресурсов подразумевает реализацию одного из двух механизмов:

  • Создание деструктора
  • Реализация классом интерфейса System.IDisposable

Создание деструкторов

Если вы вдруг программировали на языке C++, то наверное уже знакомы с концепцией деструкторов. Метод деструктора носит имя класса (как и конструктор), перед которым стоит знак тильды ( ~ ).

Деструкторы можно определить только в классах. Деструктор в отличие от конструктора не может иметь модификаторов доступа и параметры. При этом каждый класс может иметь только один деструктор.

Например, определим в классе Person простейший деструктор:

class Person < public string Name < get;>public Person(string name) => Name = name; ~Person() < Console.WriteLine($"has deleted"); > >

В данном случае в деструкторе в целях демонстрации просто выводится строка на консоль, которая уведомляет, что объект удален. Но в реальных программах в деструктор вкладывается логика освобождения неуправляемых ресурсов.

Однако на деле при очистке сборщик мусора вызывает не деструктор, а метод Finalize . Все потому, что компилятор C# компилирует деструктор в конструкцию, которая эквивалентна следующей:

protected override void Finalize() < try < // здесь идут инструкции деструктора >finally < base.Finalize(); >>

Метод Finalize уже определен в базовом для всех типов классе Object, однако данный метод нельзя так просто переопределить. И фактическая его реализация происходит через создание деструктора.

Используя в программе класс Person, после ее завершения можно будет увидеть на консоли сообщение об удалении объекта tom:

Test(); GC.Collect(); // очистка памяти под объект tom Console.Read(); // ставим задержку void Test() < Person tom = new Person("Tom"); >public class Person < public string Name < get;>public Person(string name) => Name = name; ~Person() < Console.WriteLine($"has been deleted"); > >

Обратите внимание, что даже после завершения метода Test и соответственно удаления из стека ссылки на объект Person в куче, может не последовать немедленного вызова деструктора. Лишь при завершении всей программы гарантировано произойдет очистка памяти. Однако с .NET 5 и в последующих версиях при завершении программы деструкторы не вызываются. Поэтому в программе выше для более быстрой очистки памяти применяется метод GC.Collect и для гарантированного вызова деструктора устанавливается задержка с помощью вызова Console.Read() , который ожидает от пользователя ввода.

На уровне памяти это выглядит так: сборщик мусора при размещении объекта в куче определяет, поддерживает ли данный объект метод Finalize . И если объект имеет метод Finalize, то указатель на него сохраняется в специальной таблице, которая называется очередь финализации. Когда наступает момент сборки мусора, сборщик видит, что данный объект должен быть уничтожен, и если он имеет метод Finalize, то он копируется в еще одну таблицу и окончательно уничтожается лишь при следующем проходе сборщика мусора.

Стоит отметить, что точное время вызова деструктора не определено. Кроме того, при финализации двух связанных объектов порядок вызова деструкторов не гарантируется. То есть если объект A хранит ссылку на объект B, и при этом оба эти объекта имеют деструкторы, то для объекта B деструктор моет уже отработать в то время, как для объекта A деструктор только начнет работу.

И здесь мы можем столкнуться со следующей проблемой: а что если нам немедленно надо вызвать деструктор и освободить все связанные с объектом неуправляемые ресурсы? В этом случае мы можем использовать второй подход — реализацию интерфейса IDisposable.

Интерфейс IDisposable

Интерфейс IDisposable объявляет один единственный метод Dispose , в котором при реализации интерфейса в классе должно происходить освобождение неуправляемых ресурсов. Например:

Test(); void Test() < Person? tom = null; try < tom = new Person("Tom"); >finally < tom?.Dispose(); >> public class Person : IDisposable < public string Name < get;>public Person(string name) => Name = name; public void Dispose() < Console.WriteLine($"has been disposed"); > >

В данном коде используется конструкция try. finally. По сути эта конструкция по функционалу в общем эквивалентна следующим двум строкам кода:

Person tom = new Person("Tom"); tom.Dispose();

Но конструкцию try. finally предпочтительнее использовать при вызове метода Dispose, так как она гарантирует, что даже в случае возникновения исключения произойдет освобождение ресурсов в методе Dispose.

Комбинирование подходов

Мы рассмотрели два подхода. Какой же из них лучше? С одной стороны, метод Dispose позволяет в любой момент времени вызвать освобождение связанных ресурсов, а с другой — программист, использующий наш класс, может забыть поставить в коде вызов метода Dispose. В общем бывают различные ситуации. И чтобы сочетать плюсы обоих подходов мы можем использовать комбинированный подход. Microsoft предлагает нам использовать следующий формализованный шаблон:

public class SomeClass: IDisposable < private bool disposed = false; // реализация интерфейса IDisposable. public void Dispose() < // освобождаем неуправляемые ресурсы Dispose(true); // подавляем финализацию GC.SuppressFinalize(this); >protected virtual void Dispose(bool disposing) < if (disposed) return; if (disposing) < // Освобождаем управляемые ресурсы >// освобождаем неуправляемые объекты disposed = true; > // Деструктор ~SomeClass() < Dispose (false); >>

Логика очистки реализуется перегруженной версией метода Dispose(bool disposing) . Если параметр disposing имеет значение true, то данный метод вызывается из публичного метода Dispose, если false — то из деструктора.

При вызове деструктора в качестве параметра disposing передается значение false, чтобы избежать очистки управляемых ресурсов, так как мы не можем быть уверенными в их состоянии, что они до сих пор находятся в памяти. И в этом случае остается полагаться на деструкторы этих ресурсов. Ну и в обоих случаях освобождаются неуправляемые ресурсы.

Еще один важный момент — вызов в методе Dispose метода GC.SuppressFinalize(this) . GC.SuppressFinalize не позволяет системе выполнить метод Finalize для данного объекта. Если же в классе деструктор не определен, то вызов этого метода не будет иметь никакого эффекта.

Таким образом, даже если разработчик не использует в программе метод Dispose, все равно произойдет очистка и освобождение ресурсов.

Общие рекомендации по использованию Finalize и Dispose
  • Деструктор следует реализовывать только у тех объектов, которым он действительно необходим, так как метод Finalize оказывает сильное влияние на производительность
  • После вызова метода Dispose необходимо блокировать у объекта вызов метода Finalize с помощью GC.SuppressFinalize
  • При создании производных классов от базовых, которые реализуют интерфейс IDisposable, следует также вызывать метод Dispose базового класса:
public class Derived: Base < private bool IsDisposed = false; protected override void Dispose(bool disposing) < if (IsDisposed) return; if (disposing) < // Освобождение управляемых ресурсов >IsDisposed = true; // Обращение к методу Dispose базового класса base.Dispose(disposing); > >

Финализируемые и высвобождаемые типы

Существуют два различных подхода, которые можно применять для создания класса, способного производить очистку и освобождать внутренние неуправляемые ресурсы. Первый подход заключается в переопределении метода System.Object.Finalize() и позволяет гарантировать то, что объект будет очищать себя сам во время процесса сборки мусора (когда бы тот не запускался) без вмешательства со стороны пользователя. Второй подход предусматривает реализацию интерфейса IDisposable и позволяет обеспечить пользователя объекта возможностью очищать объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать метод Dispose(), неуправляемые ресурсы могут оставаться в памяти на неопределенный срок.

Как не трудно догадаться, оба подхода можно комбинировать и применять вместе в определении одного класса, получая преимущества от обеих моделей. Если пользователь объекта не забыл вызвать метод Dispose(), можно проинформировать сборщик мусора о пропуске финализации, вызвав метод GC.SuppressFinalize(). Если же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут финализации и получит возможность освободить внутренние ресурсы. Преимущество такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда освобождаться.

Ниже приведена версия класса MyResourceWrapper, которая предусматривает выполнение и финализации, и освобождения:

// Сложный упаковщик ресурсов. public class MyResourceWrapper : IDisposable < // Сборщик мусора будет вызывать этот метод, если // пользователь объекта забыл вызвать метод Dispose(). ~MyResourceWrapper() < // Освобождение любых внутренних неуправляемых // ресурсов. Метод Dispose() НЕ должен вызываться // ни для каких управляемых объектов. >// Пользователь объекта будет вызывать этот метод для // того, чтобы освободить ресурсы как можно быстрее. public void Dispose() < // Здесь осуществляется освобождение неуправляемых ресурсов // и вызов Dispose() для остальных высвобождаемых объектов. // Если пользователь вызвал Dispose(), то финализация не нужна, // поэтому далее она подавляется. GC.SuppressFinalize(this); >>

Здесь важно обратить внимание на то, что метод Dispose() был модифицирован так, чтобы вызывать метод GC.SuppressFinalize(). Этот метод информирует CLR-среду о том, что вызывать деструктор при подвергании данного объекта сборке мусора больше не требуется, поскольку неуправляемые ресурсы уже были освобождены посредством логики Dispose().

Формализованный шаблон очистки

В текущей реализации MyResourceWrapper работает довольно хорошо, но все равно еще остается несколько небольших недочетов. Во-первых, методам Finalize() и Dispose() требуется освобождать одни и те же неуправляемые ресурсы, а это чревато дублированием кода, которое может существенно усложнить его сопровождение. Поэтому в идеале не помешало бы определить приватную вспомогательную функцию, которая могла бы вызываться в любом из этих методов.

Во-вторых, нелишне позаботиться о том, чтобы метод Finalize() не пытался избавиться от любых управляемых объектов, а метод Dispose() — наоборот, обязательно это делал. И, наконец, в-третьих, не помешало бы позаботиться о том, чтобы пользователь объекта мог спокойно вызывать метод Dispose() множество раз без получения ошибки. В настоящий момент в методе Dispose() никаких подобных мер предосторожностей пока не предусмотрено.

Для решения подобных вопросов с дизайном в Microsoft создали формальный шаблон очистки, который позволяет достичь оптимального баланса между надежностью, удобством в обслуживании и производительностью. Ниже приведена окончательная версия MyResourceWrapper, в которой применяется упомянутый формальный шаблон:

public class MyResourceWrapper : IDisposable ( // Используется для выяснения того, вызывался ли уже метод Dispose() private bool disposed = false; public void Dispose () < // Вызов вспомогательного метода. // Значение true указывает на то, что очистка // была инициирована пользователем объекта. Cleanup(true); // Подавление финализации. GC.SuppressFinalize (this); >private void Cleanup(bool disposing) < // Проверка, выполнялась ли очистка, if (!this.disposed) < // Если disposing равно true, должно осуществляться // освобождение всех управляемых ресурсов, if (disposing) < // Здесь осуществляется освобождение управляемых ресурсов. >// Очистка неуправляемых ресурсов. > disposed = true; > ~MyResourceWrapper() < // Вызов вспомогательного метода. // Значение false указывает на то, что // очистка была инициирована сборщиком мусора. Cleanup(false); >>

Обратите внимание, что в MyResourceWrapper теперь определяется приватный вспомогательный метод по имени Cleanup(). Передача ему в качестве аргумента значения true свидетельствует о том, что очистку инициировал пользователь объекта, следовательно, требуется освободить все управляемые и неуправляемые ресурсы. Когда очистка инициируется сборщиком мусора, при вызове CleanUp() передается значение false, чтобы освобождения внутренних высвобождаемых объектов не происходило (поскольку рассчитывать на то. что они по-прежнему находятся в памяти, нельзя). И, наконец, перед выходом из Cleanup() для переменной экземпляра типа bool (по имени disposed) устанавливается значение true, что дает возможность вызывать метод Dispose() много раз без появления ошибки.

После «освобождения» (dispose) объекта клиент по-прежнему может вызывать на нем какие-нибудь члены, поскольку объект пока еще находится в памяти. Следовательно, в показанном сложном классе-упаковщике ресурсов не помешало бы снабдить каждый член дополнительной логикой, которая бы, по сути, гласила: «если объект освобожден, ничего не делать, а просто вернуть управление».

Высвобождаемые объекты

Методы финализации могут применяться для освобождения неуправляемых ресурсов при активизации процесса сборки мусора. Однако многие неуправляемые объекты являются «ценными элементами» (например, низкоуровневые соединения с базой данных или файловые дескрипторы) и часто выгоднее освобождать их как можно раньше, еще до наступления момента сборки мусора. Поэтому вместо переопределения Finalize() в качестве альтернативного варианта также можно реализовать в классе интерфейс IDisposable, который имеет единственный метод по имени Dispose():

public interface IDisposable

Когда действительно реализуется поддержка интерфейса IDisposable, то предполагается, что после завершения работы с объектом метод Dispose() должен вручную вызываться пользователем этого объекта, прежде чем объектной ссылке будет позволено покинуть область действия. Благодаря этому объект может выполнять любую необходимую очистку неуправляемых ресурсов без попадания в очередь финализации и без ожидания того, когда сборщик мусора запустит содержащуюся в классе логику финализации.

Интерфейс IDisposable может быть реализован как в классах, так и в структурах (в отличие от метода Finalize(), который допускается переопределять только в классах), потому что метод Dispose() вызывается пользователем объекта (а не сборщиком мусора). Рассмотрим пример использования этого интерфейса:

using System; namespace ConsoleApplication1 < // Данный класс реализует интерейс IDisposable class FinalizeObject : IDisposable < public int id < get; set; >public FinalizeObject(int id) < this.id = id; >// Реализуем метод Dispose() public void Dispose() < Console.WriteLine("Высвобождение объекта!"); >> class Program < static void Main(string[] args) < FinalizeObject obj = new FinalizeObject(4); obj.Dispose(); Console.Read(); >> >

Обратите внимание, что метод Dispose() отвечает не только за освобождение неуправляемых ресурсов типа, но и за вызов аналогичного метода в отношении любых других содержащихся в нем высвобождаемых объектов. В отличие от Finalize(), в нем вполне допустимо взаимодействовать с другими управляемыми объектами. Объясняется это очень просто: сборщик мусора не имеет понятия об интерфейсе IDisposable и потому никогда не будет вызывать метод Dispose(). Следовательно, при вызове данного метода пользователем объект будет все еще существовать в управляемой куче и иметь доступ ко всем остальным находящимся там объектам.

Этот пример раскрывает еще одно правило относительно работы с подвергаемыми сборке мусора типами: для любого создаваемого напрямую объекта, если он поддерживает интерфейс IDisposable, следует всегда вызывать метод Dispose(). Необходимо исходить из того, что в случае, если разработчик класса решил реализовать метод Dispose(), значит, классу надлежит выполнять какую-то очистку.

Повторное использование ключевого слова using в C#

При работе с управляемым объектом, который реализует интерфейс IDisposable, довольно часто требуется применять структурированную обработку исключений, гарантируя, что метод Dispose() типа будет вызываться даже в случае возникновения какого-то исключения:

FinalizeObject obj = new FinalizeObject(4); try < // Выполнение необходимых операций >finally

Хотя это является замечательными примером «безопасного программирования», истина состоит в том, что очень немногих разработчиков прельщает перспектива заключать каждый очищаемый тип в блок try/finally лишь для того, чтобы гарантировать вызов метода Dispose(). Для достижения аналогичного результата, но гораздо менее громоздким образом, в C# поддерживается специальный фрагмент синтаксиса, который выглядит следующим образом:

using (FinalizeObject obj = new FinalizeObject(4)) < // Необходимые действия >

Если теперь просмотреть CIL-код этого метода Main() с помощью утилиты ildasm.ехе, то обнаружится, что синтаксис using в таких случаях на самом деле расширяется до логики try/finally, которая включает в себя и ожидаемый вызов Dispose():

CIL-код высвобождаемых объектов C#

Хотя применение такого синтаксиса действительно избавляет от необходимости вручную помещать высвобождаемые объекты в рамки try/finally, в настоящее время, к сожалению, ключевое слово using в C# имеет двойное значение (поскольку служит и для добавления ссылки на пространства имен, и для вызова метода Dispose()). Тем не менее, при работе с типами .NET, которые поддерживают интерфейс IDisposable, данная синтаксическая конструкция будет гарантировать автоматический вызов метода Dispose() в отношении соответствующего объекта при выходе из блока using.

Кроме того, в контексте using допускается объявлять несколько объектов одного и того же типа. Как не трудно догадаться, в таком случае компилятор будет вставлять код с вызовом Dispose() для каждого объявляемого объекта.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *