IDisposable. Dispose Метод
Определение
Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
Выполняет определяемые приложением задачи, связанные с удалением, высвобождением или сбросом неуправляемых ресурсов.
Примеры
В следующем примере показано, как можно реализовать Dispose метод.
Комментарии
Используйте этот метод, чтобы закрыть или освободить неуправляемые ресурсы, такие как файлы, потоки и дескрипторы, удерживаемые экземпляром класса, реализующего этот интерфейс. По соглашению этот метод используется для всех задач, связанных с освобождением ресурсов, удерживаемых объектом, или подготовки объекта к повторному использованию.
При реализации этого метода убедитесь, что все удерживаемые ресурсы освобождаются путем распространения вызова через иерархию вложений. Например, если объект A выделяет объект B, а объект б выделяет объект C, то Dispose Реализация метода должна вызываться Dispose в B, который должен в свою очередь вызывать Dispose на языке c.
Компилятор C++ поддерживает детерминированное удаление ресурсов и не допускает прямой реализации Dispose метода.
Если Dispose метод объекта вызывается более одного раза, объект должен игнорировать все вызовы после первого. Объект не должен вызывать исключение, если его Dispose метод вызывается несколько раз. Методы экземпляра, отличные от, Dispose могут вызывать исключение, ObjectDisposedException когда ресурсы уже удалены.
Поскольку Dispose метод должен вызываться явным образом, всегда существует опасность того, что неуправляемые ресурсы не будут освобождены, так как потребитель объекта не может вызвать его Dispose метод. Этого можно избежать двумя способами:
Реализуйте метод завершения для освобождения ресурсов, если Dispose не вызывается. По умолчанию сборщик мусора автоматически вызывает метод завершения объекта перед освобождением его памяти. Однако при Dispose вызове метода обычно не требуется, чтобы сборщик мусора вызывал метод завершения Dispose объекта. Чтобы предотвратить автоматическое завершение, Dispose реализации могут вызывать GC.SuppressFinalize метод.
Programming stuff
Страницы
понедельник, 12 августа 2013 г.
О явном вызове метода Dispose
Но в его книгах (в особенности CLR via C#) есть ряд не вполне адекватных советов по дизайну наших с вами приложений. Так, Рихтер весьма категоричен при описании паттерна Dispose, который вполне валиден для разработчика библиотек, но сомнителен для разработчиков приложений.
И вот еще один весьма сомнительный совет по поводу необходимости явного вызова метода Dispose:
“Важно. В общем случае, я очень не рекомендую явно вызывать метод Dispose в вашем коде. Дело в том, что сборщик мусора CLR хорошо написан, и вы позволить ему делать свою работу. Сборщик мусора знает, когда объект становится недостижимым из кода приложения, и только тогда уничтожит его. Когда явно вызывается метод Dispose, то, по сути, это говорит, что приложению объект больше не нужен. Но во многих приложениях невозможно знать точно, что объект больше не используется.
Предположим, ваш код создает новый объект и вы передаете ссылку на этот объект в другой метод. Этот метод может сохранить ссылку на этот объект в некотором внутреннем поле (в корневой ссылке) и вызывающий метод никак не может узнать об этом. Конечно, вызывающий метод может вызвать Dispose, но потом, некоторый код может попытаться использовать этот объект и получит ObjectDisposeException. Я рекомендую явно вызывать метод Dispose только тогда, когда вы точно знаете, что в этом месте требуется очистка ресурсов (как в случае попытки удаления открытого файла).”
Джеффри Рихтера, “CLR via C#”, глава 21 “The Managed Heap and Garbage Collection”
Вообще, идея, чтобы каждый делал свою работу мне очень нравится;), и я согласен с советами, которые говорят о вреде в большинстве случаев явного вызова сборки мусора через GC.Collect. Но мне совсем не понятно, как можно говорить, чтобы «в общем случае» разработчики избегали вызова метода Dispose?
Давайте подумаем о том, в каких случаях мы используем disposable объекты и когда стоит вызывать метод Dispose, а когда нет.
1. Использование ресурсов в блоке using или идиома RAII (Resource Acquisition Is Initialization).
Напомню, что идиома RAII пришла из языка С++, в котором она является основой автоматического управления памятью и ресурсами. Ее идея в том, что в конструкторе объекта захватывается некоторый ресурс, а в деструкторе – он освобождается. При этом используется тот факт, что деструктор объекта будет вызван при выходе из области видимости автоматически, не зависимо от причина выхода из этой самой области видимости: выход по ключевому слову return или благодаря генерации исключения.
С моей точки зрения именно этот способ является наиболее распространенным и предпочтительным паттерном использования ресурсов. Ведь не зря, камрады из Редмонда так хвалятся тем, что благодаря новым асинхронным возможностям мы теперь можем пользоваться блоком using в наших асинхронных приложениях.
2. Использование ресурсов в качестве полей других объектов
Помимо использования в качестве локальной переменной, мы можем положить disposable объект в виде поля нашего класса. В этом случае наш класс тоже должен реализовать интерфейс IDisposable, который затем может использоваться в виде локальной переменной или в виде поля другого объекта.
В обоих этих случаях мы можем столкнуться с проблемой, поднятой Джеффри Рихтером и передать наш ресурс некоторому методу, который его сохранит. С другой стороны, подобное использование ресурсов противоречит всем канонам, ведь этот самый disposable объект является деталью реализации нашего метода или класса, и передача его куда-то во внешний мир в большинстве случаев является не лучшим решением.
С подобными рассуждениями мы можем прийти к выводу, что такие понятия как инварианты класса тоже не стоит использовать, ведь из конструктора вы можете вызвать некоторый метод и передать ему «себя» в недостроенном виде.
Тем более, что помимо приведенных случаев, есть еще один вариант использования паттерна Dispose.
3. Использование Disposable объектов не для управления ресурсами
Но в наших с вами дот нетах есть и исключения. Так, например, класс Task содержит финализатор, но не содержит неуправляемых ресурсов (да, таска может содержать управляемые ресурсы, но сейчас не об этом). В случае задач финализатор используется не для очистки ресурсов, а чтобы понять, является ли исключение текущей задачи «необработанным».
Помимо этого, есть примеры использования disposable объектов не для управления ресурсами напрямую, а для выполнения некоторых других операций.
3.1. Отписка от событий
Так, если ваш класс в конструкторе подписывается на событие долгоживущего объекта, то чтобы избежать утечки памяти нам нужно в некоторый момент времени от них отписаться. В этом случае финализатор нам никак не поможет, поскольку чтобы он вызвался нам нужно вначале отписаться от глобального события. В этом случае вполне валидным решением является использование для этих целей метода Dispose:
3.2. Освобождение блокировок или другие «постдействия»
Помимо отписки от событий или явного освобождения ресурсов метод Dispose и блок using может использовать для любых других произвольных действий. Например, мы можем использовать disposable оболочку для освобождения блокировок, аналогично тому, как это делается в С++:
Так, для класса ReaderWriterLockSlim мы можем сделать методы расширения, которые будут захватывать блокировку в конструкторе и освобождать ее в методе Dispose:
Да, некоторые товарищи, в том числе Эрик Липперт не одобряет использование блока using не для управления ресурсами, но иногда именно такой подход бывает довольно удобным.
Когда можно не вызывать Dispose?
А есть ли случаи, когда вызывать Dispose не обязательно? Да, есть, конечно.
Некоторые классы могут реализовывать интерфейс IDisposable, но при этом могут аллоцировать ресурсы лишь в редких случаях. Упомянутые вышеTask и Task реализуют IDisposable, но по словам Стивена Тауба (одного из авторов TPL) вызывать Dispose для них не нужно, поскольку ресурсы внутри объекта Task выделяются лишь в редких случаях.
Некоторые классы реализуют IDisposable, который достался от базового класса; таким примером является класс StringReader, который по своей природе не владеет ресурсами.
Бывают случаи, когда класс владеет некритическими ресурсами, при этом алгоритм владения является нечетким. Таким примером является класс ImageList из Windows Forms, который не очищает все содержащиеся в нем изображения.
В большинстве случаев и правда крайне не рекомендуется вызывать метод Dispose на объектах, созданных не вами. Также может быть опасным вызывать метод Dispose в многопоточных сценариях, поскольку это может привести к гонкам. Но в этом случае я бы скорее постарался ограничить многопоточное использование ресурса в одном классе, что позволит перед явной очисткой нашего ресурса дождаться завершения всех рабочих потоков.
Другими словами, случаи когда вы можете не вызывать метод Dispose и правда существуют, но я бы не назвал это явление таким уж распространенным. Дизайн класса говорит о намерениях проектировщика и реализация интерфейса IDisposable говорит клиентам класса, что объект поддерживает явное освобождение ресурсов. При этом не зная о о последствиях отсутствия вызова Dispose я бы рассматривал наиболее пессимистический вариант и старался бы освободить ресурсы явным образом.
Заключение
Если уж говорить о рекомендациях по вызову метода Dispose, то я бы озвучил совет так, как это сделал Джо Даффи, в замечательной книге “Framework Design Guidelines”:
“Если тип реализует интерфейс IDisposable и владение ресурсами очевидно (к чему в большинстве случаев стоит стремиться, прим. С.Т.), вы должны сделать все возможное для вызова метода Dispose после завершения использования объекта. Но если владение ресурсами неочевидно (поскольку объект используется во множестве мест или используется из нескольких потоков), то отсутствие вызова Dispose не причинит особого вреда. (Помимо ряда неприятных случаев, типа класса FileStream, когда отсутствие вызова метода Dispose может привести к непредсказуемым последствиям).”
Дополнительные ссылки
23 комментария:
Даффи: «Если тип реализует интерфейс IDisposable и владение ресурсами очевидно, вы должны сделать все возможное для вызова метода Dispose после завершения использования объекта»
Рихтер: «Я рекомендую явно вызывать метод Dispose только тогда, когда вы точно знаете, что в этом месте требуется очистка ресурсов»
То, что вы написали, просто показывает как можно использовать Dispose. Почему вы против слов Рихтера и за слова Даффи, и почему вы считает что ваши доводы против слов Рихтера и за доводы Даффи категорически непонятно.
Вадим, посмотрите на акценты в этих советах.
Джеффри пишет, что в подавляющем случае вызывать Dispose не нужно, но иногда можно.
Даффи пишет, чтобы вы сделали все для того, чтобы управлять ресурсами явно, но не переживали, если вас сделать этого не удается.
ИМХО это большая разница, поскольку опыт подсказывает, что зачастую код не расшаривает disposable объекты с другими объектами/потоками, что делает их явное освобождение предпочтительным.
Более того, в некоторых случаях (отписка от событий, освобождение блокировок/мьютексов, закрытие файлов/сокетов) отсутствие явного освобождения ресурсов приведет к непредсказуемому поведению приложения.
А чем мои доводы не устраивают? Я пишу о том, что Рихтер завязался на граничный случай применения disposable объектов. Вы с этим не согласны?
Позвольте небольшое дополнение по поводу 3.1.
Следует подчеркнуть: Dispose не просто что-то там чистит. Вызов Dispose подразумевает завершение работы с экземпляром объекта.
Если, допустим, Dispose нужен только для отписки от событий (и мы об этом знаем), то возможен следующий вариант:
1. Создаём экземпляр объекта.
2. Экземпляр подписывается на какие-то события.
3. Используем этот объект.
4. Вызываем Dispose.
5. Объект не выбрасываем, вместо этого возвращаемся к п.2.
Пример из жизни: в популярной библиотеке MVVM Light для этих целей существует специальный интерфейс ICleanup.
http://stackoverflow.com/questions/2963151/cleanup-vs-disposebool-in-mvvm-light
Всё это, конечно, не означает, что Dispose не должен уметь корректно отписываться.
Ммм. Видимо мы с вами воспринимаем текст через призму своего Я.
Я согласен с тем, что Dispose нужно вызывать только в том случае, если вы знаете что произойдет. Во всех остальных случая делать этого не стоит. Будь то закрытие транзакции, освобождение блокировок или просто очистка ресурсов.
Вадим, мне все зе кажется, что подход должен быть противоположным. Мы должны вызывать Dispose всегда и не вызывать его лишь тогда, когда точно знаем, что вреда от этого не будет.
В общем же случае, мы просто не знаем, будут или нет негативные последствия отсутствия вызова Dispose, поэтому мы должны приложить все усилия, чтобы вызвать этот метод.
В вашем примере я вызову Dispose если вы явно скажете что после использования вашего объекта желательно освободить ресурсы, либо в Dispose закрывается транзакция, либо еще что-то. Возможно что я предположу что закрыть транзакцию или освободить ресурсы удобно в Dispose, посмотрю что вы делаете в Dispose и только после этого задумаюсь о использовании Dispose.
Если мне не сказали что происходит в Dispose и что это удобно использовать,если объект не намекает на использование Dispose, я даже не посмотрю реализует ли объект IDisposable.
И все таки, Рихтер и Даффи говорят одно и тоже.
Даффи: «Если тип реализует интерфейс IDisposable и владение ресурсами очевидно, вы должны сделать все возможное для вызова метода Dispose после завершения использования объекта»
Рихтер: «Я рекомендую явно вызывать метод Dispose только тогда, когда вы точно знаете, что в этом месте требуется очистка ресурсов»
Т.е. если ты уверен, то вызывай, не уверен не вызывай. Если вы не вызовите Dispose, это не страшно, об этом говорят оба.
ДАффи: «Но если владение ресурсами неочевидно (поскольку объект используется во множестве мест или используется из нескольких потоков), то отсутствие вызова Dispose не причинит особого вреда.»
Так что это скорее вы приравниваете Dipose к деструктору, говоря что вызывать надо обязательно.
Вадим, Даффи и Рихтер говорят необ одном и том же. Даффи говорит об очевидности владения речурсами (мои первый и второй случай из статьи), а Рихтер говорит о том, что вы знаете, что эта очистка нужна. Это две совершенно разные вещи.
И я не приравниваю диспоз с деструктором, я говорю о том, что если я делаю класс диспозабл, то самим дизайном класса я говорю о важности вызова этого метода. Если класс реализует этот интерфейс, то это автоматом говорит всем его клиентам, что вызывать метод надо (пока документация или опыт использования не говорит обратного).
Не верно использовать Dispose не зная что там и зачем.
Яркий пример тому отложенная загрузка из какого либо источника. Источник реализует IDisposable. После этого из источника вы получаете объект, который загрузит данные только по требованию. Вызов Dispose у источника до того, как вы обратились к объекту с отложенной загрузкой есть ошибка. А источником может быть что угодно. И спрятать я могу источник за другими IDisposable объектами, которые в итоге вызовут Dispose у источника. И вызов Dispose у объекта «обертки» по вашему мне надо вызывать всегда, не зная что там происходит? И таких примеров может быть куча.
Вы ведь не можете быть уверены в том, что делает другой объект, только если вам явно не указали что он делает или если его написали не вы. И так как Dispose это такой же метод как и все, спрятать за ним можно все что угодно. Закрытие соединения, транзакции, разлочивание объекта. И делать все это вы должны только тогда, когда вам это нужно. Будь то даже работа с FileStream. Dispose нужен для удобства, что бы не забывать закрывать поток. Но вы это можете сделать и без Dispose, просто вызвав close. Т.е. обязательства вызывать Dispose нет. Более того, если бы я НЕ знал что там происходит, я бы не вызывал Dispose, т.к. название метода Dispose не дает мне ровным счетом никакого представления о том, что он делает. А вдруг он не закрывает FileStream, а уничтожает полностью файл? Ведь Dispose это не просто очистка памяти, это какое то действие, которое произойдет в момент вызова метода Dispose, или в момент выхода из оператора Using.
И опять таки про FileStream, т.к. этот объект будет владеть неуправляемыми ресурсами, то у него реализован финализатор. Т.е. если не вызывать Dispose, то «великого зла» не произойдет.
Для меня, реализация IDisposable, говорит лишь о том, что я могу этот объект использовать в using с удобством для себя(примеров может быть куча: транзакции, логирование, работа в рамках какого либо контекста, да даже профилирование). Но это не обязывает меня использовать Dispose. Это можно делать лишь тогда, когда мне это нужно и тогда когда я знаю что произойдет, когда меня об этом предупредили или объяснили что это обязательно.
И я не приравниваю диспоз с деструктором, я говорю о том, что если я делаю класс диспозабл, то самим дизайном класса я говорю о важности вызова этого метода.
Очень спорно. А если это класс логгер, или профайлер, или генеация HTML? И реализация IDisposable только для возможности использовать синтаксический сахар? И вызов Dispose «очень сильно» не обязателен?
Более того, ничего «криминального» не произойдет если ваш объект владеет управляемыми ресурсами. Другое дело неуправляемые ресурсы, но тут вы будете реализовывать финализатор, и опять таки Dispose можно вообще не реализовывать (хотя это будет не очень красиво).
Реализация метода Dispose
Реализация метода Dispose в основном используется для освобождения неуправляемых ресурсов. При работе с членами экземпляра, которые являются реализациями IDisposable, обычно применяются каскадные вызовы Dispose. Существуют дополнительные причины для реализации Dispose, включая освобождение выделенной памяти, удаление добавленного в коллекцию элемента или информирование о снятой блокировке.
Чтобы обеспечить нормальную очистку таких ресурсов, метод Dispose должен быть идемпотентным, то есть поддерживать многократный вызов без создания исключений. Кроме того, последующие вызовы Dispose не должны выполнять никаких действий.
В приведенном для метода GC.KeepAlive примере показано, как сборка мусора может привести к выполнению метода завершения, в то время как по-прежнему будет использоваться неуправляемая ссылка на объект или его члены. Возможно, имеет смысл использовать GC.KeepAlive, чтобы запретить сборку мусора для объекта с момента начала текущей процедуры до вызова этого метода.
Безопасные дескрипторы
Написание кода для метода завершения объекта является сложной задачей, которая может вызвать проблемы при неправильном выполнении. Поэтому вместо реализации метода завершения рекомендуется создавать объекты System.Runtime.InteropServices.SafeHandle.
System.Runtime.InteropServices.SafeHandle — это абстрактный управляемый тип, выполняющий роль оболочки для System.IntPtr, который идентифицирует неуправляемый ресурс. В среде Windows он может обозначать дескриптор, а в среде UNIX — дескриптор файла. Он обеспечивает всю логику, которая гарантирует, что при удалении SafeHandle или уничтожении всех ссылок на SafeHandle и завершении экземпляра SafeHandle ресурс будет освобожден один и только один раз.
Безопасные дескрипторы предоставляются следующими производными классами в пространстве имен Microsoft.Win32.SafeHandles:
Dispose() и Dispose(bool)
невиртуальный ( public в Visual Basic) метод IDisposable.Dispose с атрибутом NonInheritable и без параметров;
метод Dispose с атрибутом protected virtual ( Overridable в Visual Basic) со следующей сигнатурой:
Метод Dispose()
Поскольку невиртуальный ( public в Visual Basic) метод Dispose с атрибутом NonInheritable без параметров вызывается объектом-получателем типа, его назначение состоит в том, чтобы освободить неуправляемые ресурсы и указать, что метод завершения, если он задан, не должен выполняться. Освобождение физической памяти, связанной с управляемым объектом, всегда оставляется сборщику мусора. Он имеет стандартную реализацию:
Перегрузка метода Dispose(Boolean)
В этой перегрузке параметр disposing типа Boolean указывает, откуда осуществляется вызов метода: из метода Dispose (значение true ) или из метода завершения (значение false ).
Тело метода состоит из двух блоков кода:
Управляемые объекты, реализующие IDisposable. Условный блок может использоваться для вызова реализации Dispose (каскадное удаление). При использовании класса, производного от System.Runtime.InteropServices.SafeHandle, в качестве оболочки для неуправляемого ресурса необходимо вызвать реализацию SafeHandle.Dispose().
Если метод вызывается из метода завершения, должен выполняться только тот код, который освобождает неуправляемые ресурсы. Разработчик обязан следить, чтобы эта ветвь кода не взаимодействовала с управляемыми объектами, которые могли быть освобождены. Это важно, поскольку порядок, в котором сборщик мусора уничтожает управляемые объекты в процессе завершения, не детерминирован.
Каскадные вызовы Dispose
Если класс имеет собственное поле или свойство, а его тип реализует IDisposable, сам класс также обязан реализовывать IDisposable. Класс, который создает экземпляр реализации IDisposable и сохраняет его в качестве члена экземпляра, обязан самостоятельно очищать его. Это поможет гарантировать, что удаляемые ссылочные типы получат возможность выполнять очистку детерминированно с помощью метода Dispose. В нашем примере представлен класс sealed (или NotInheritable в Visual Basic).
Реализация шаблона освобождения
Все незапечатанные классы (в Visual Basic это классы, не имеющие модификатора NotInheritable ) должны считаться потенциальным базовым классом, так как они поддерживают наследование. При реализации шаблона освобождения для любого класса, который может быть базовым, необходимо обеспечить следующее:
Базовый класс может ссылаться только на управляемые объекты и реализовывать шаблон освобождения. В таких случаях метод завершения не нужен. Метод завершения нужен только в том случае, если используются прямые ссылки на неуправляемые ресурсы.
Вот общий шаблон реализации шаблона удаления для базового класса, который использует безопасный дескриптор.
В предыдущем примере используется объект SafeFileHandle для иллюстрации шаблона. Вместо него может использоваться любой объект, производный от SafeHandle. Обратите внимание, что в этом примере неправильно создаются экземпляры объекта SafeFileHandle.
Вот общий шаблон реализации шаблона удаления для базового класса, который переопределяет метод Object.Finalize.
Реализация шаблона освобождения для производного класса
Класс, производный от класса, реализующего интерфейс IDisposable, не должен реализовывать интерфейс IDisposable, поскольку реализация метода IDisposable.Dispose базового класса наследуется производными классами. Вместо этого для очистки производного класса необходимо предоставить следующее:
Вот общий шаблон реализации шаблона удаления для производного класса, который использует безопасный дескриптор:
В предыдущем примере используется объект SafeFileHandle для иллюстрации шаблона. Вместо него может использоваться любой объект, производный от SafeHandle. Обратите внимание, что в этом примере неправильно создаются экземпляры объекта SafeFileHandle.
Вот общий шаблон реализации шаблона удаления для производного класса, который переопределяет метод Object.Finalize:
Реализация шаблона освобождения с безопасными дескрипторами
Реализация шаблона освобождения для производного класса с безопасными дескрипторами
Dispose pattern
“Не стоит следовать некоторой идиоме только потому, что так делают все или так где-то написано”
Мысли автора статьи во время чтения и рефакторинга чужого кода
А как же финализаторы? – спросите вы. Ну, да, есть такое дело, финализаторы действительно предназначены для освобождения ресурсов, но проблема в том, что время их вызова не детерминировано, а это значит, что никто не знает, когда они будут вызваны и будут ли вызваны вообще. Да и порядок вызова финализаторов не определен, поэтому при вызове финализатора некоторые «части» вашего объекта уже могут быть «разрушены», поскольку их финализаторы уже были вызваны. В общем, финализаторы – они-то есть, но это скорее «страховочный трос», а не нормальное средство управления ресурсами.
Идиома RAII
В языке С++, в котором нет никаких встроенных средств для автоматического управления памятью помимо умных указателей, уже давно активно применяется паттерн (или идиома) для своевременного освобождения ресурсов (*). Эта идиома носит название «Захват ресурса есть инициализация» (RAII — Resource Acquisition Is Initialization) и заключается в следующем. Ресурс захватывается в конструкторе и освобождается в деструкторе, а поскольку деструкторы вызываются автоматически, то и дополнительных усилий по управлению ресурсами больше не требуется.
Управляемые и неуправляемые ресурсы
class NativeResourceWrapper : IDisposable
<
// IntPtr – описатель неуправляемого ресурса
private IntPtr nativeResourceHandle;
public NativeResourceWrapper()
<
// «Захватываем» неуправляемый ресурс путем вызова специальной функции
nativeResourceHandle = AcquireNativeResource();
>
public void Dispose()
<
// Освобождаем захваченный ресурс, опять же, путем вызова какой-то
// специальной функции
ReleaseNativeResource(nativeResourceHandle);
>
// Есть еще и финализатор, но его роль будет раскрыта позднее
Таким образом, любой объект может владеть ресурсами двух типов: он может непосредственно содержать неуправляемый ресурс (например, IntPtr) или же он может содержать ссылку на управляемый ресурс (например, NativeResourceWrapper), при этом в обоих случаях объект, содержащий один из этих ресурсов, сам становится управляемым ресурсом. Это может показаться не слишком принципиальным, но понимать разницу между двумя типами ресурсов очень важно, поскольку работать с ними приходится по-разному.
Dispose pattern
Итак, мы знаем, что объект может владеть двумя типами ресурсов: управляемыми и неуправляемыми; а также то, что у нас есть два способа освобождения ресурсов: детерминированный, с помощью метода Dispose и недетерминированный, с помощью финализатора (***). А теперь давайте посмотрим, как со всем этим добром жить и, главное, как это добро освобождать.
Идея Dispose паттерна состоит в следующем: давайте мы всю логику освобождения ресурсов поместим в отдельный метод, и будем вызывать его и из метода Dispose, и из финализатора, при этом, давайте добавим флажок, который бы говорил нам о том, кто вызвал этот метод. Поскольку эта простая идея содержит довольно большое количество подробностей, то давайте изложим Dispose паттерн по пунктам.
1. Класс, содержащий управляемые или неуправляемые ресурсы реализует интерфейс IDisposable
2. Класс содержит метод Dispose(booldisposing), который и делает всю работу по освобождению ресурсов; параметр disposing говорит о том, вызывается ли этот метод из метода Dispose или из финализатора. Этот метод должен быть protectedvirtual для не-sealed классов и private для sealed классов
// Для не-sealed классов
protected virtual void Dispose( bool disposing) <>
3. Метод Dispose всегда реализуется следующим образом: вначале вызывается метод Dispose(true), а затем может следовать вызов метода GC.SuppressFinalize(), который предотвращает вызов финализатора.
Метод GC.SuppressFinalize(), во-первых, должен вызываться после вызова Dispose(true), а не перед ним, поскольку если метод Dispose(true) «упадет» с исключением, то выполнение финализатора не отменится. Во-вторых, GC.SuppressFinalize() должен вызываться даже для классов, не содержащих финализаторы, поскольку финализатор может появиться у его наследника (т.е. мы должны вызывать метод GC.SuppressFinalize() во всех не-sealed классах).
4. Метод Dispose(booldisposing) содержит две части: (1) если этот метод вызван из метода Dispose (т.е. параметр disposing равен true), то мы освобождаем управляемые и неуправляемые ресурсы и (2) если метод вызван из финализатора во время сборки мусора (параметр disposing равен false), то мы освобождаем только неуправляемые ресурсы.
void Dispose( bool disposing)
<
if (disposing)
<
// Освобождаем только управляемые ресурсы
>
5. (ОПЦИОНАЛЬНО) Класс может содержать финализатор и вызывать из него Dispose(booldisposing) передавая false в качестве параметра.
Не забывайте, что финализатор может быть вызван даже для частично сконструированных объектов, если конструктор этого класса сгенерирует исключение. Так что код очистки неуправляемых ресурсов должен учитывать то, что ресурсы еще не захвачены (****).
6. (ОПЦИОНАЛЬНО) Класс может содержать поле bool _disposed, которое говорит о том, что ресурсы объекта уже освобождены. Disposable-классы должны спокойно позволять повторный вызов метода Dispose, а также генерировать исключение при доступе к любым другим публичным методам или свойствам (поскольку инвариант объекта уже разрушен).
void Dispose( bool disposing)
<
if (disposed)
return ; // Ресурсы уже освобождены
// Освобождаем ресурсы
disposed = true ;
>
7. (ОПЦИОНАЛЬНО) Класс может наследовать от CriticalFinalizerObject, если предыдущих шести пунктов мало и вы хотите большей экзотики. Наследование от этого класса дает вам дополнительные гарантии:
Прагматичный взгляд на Dispose паттерн
Вся сложности реализации Dispose паттерна связаны с предположением о том, что один и тот же класс (или иерархия классов) может одновременно содержать как управляемые, так и неуправляемые ресурсы. Но давайте подумаем, а зачем вообще нам может понадобиться хранить неуправляемые ресурсы напрямую в классах бизнес-логики? А как же пресловутые Принципы Единой Ответственности (SRP – Single Responsibility Principle) и Здравого Смысла? Идиома RAII, описанная ранее, успешно используется десятки лет и предназначена как раз для таких случаев: если у вас есть неуправляемый ресурс, то вместо того, чтобы работать с ним напрямую, оберните его в управляемую оболочку и работайте уже с нею.
Все это я веду к тому, что не нужно смешивать в вашем коде бизнес-логику и логику по работе с неуправляемыми ресурсами. И то, и другое достаточно сложно само по себе и заслуживает отдельного класса. Вот и получается, что данный паттерн «оптимизирован» на очень редкий случай (что класс может содержать и управляемые и неуправляемые ресурсы), при этом делая наиболее распространенный случай, когда класс содержит только управляемые ресурсы, очень неудобной в реализации и сопровождении.
Упрощенная версия Dispose паттерна
Если мы с вами знаем, что ни один человек не собирается смешивать управляемые и неуправляемые ресурсы в одном месте, так почему бы не реализовать это в коде явным образом? Мы можем оставить метод Dispose и вместо дополнительного метода Dispose с совсем невнятным булевым параметром, добавить виртуальный метод DisposeManagedResources, имя которого будет четко говорить о том, что мы должны освободить именно управляемые ресурсы. Модификатор доступа этого метода должен быть аналогичным нашему методу Dispose(bool), т.е. protected virtual для не-sealed классов или private для sealed классов.
class SomethingWithManagedResources : IDisposable
<
public void Dispose()
<
// Никаких Dispose(true) и никаких вызовов GC.SuppressFinalize()
DisposeManagedResources();
>
С первого взгляда такой подход может показаться слишком уж прагматичным, однако посудите сами: в книге Framework Design Guidelines описанию Dispose паттерна посвящено два десятка страниц, при этом ее авторы рекомендуют добавлять финализаторы только в случае острой необходимости. При этом все мы знаем, что смешивать два типа ресурсов в одном классе плохо, но все же следуем паттерну, который это поощряет, а не запрещает.
Заключение
При разработке библиотечных классов или бизнес-классов, которые будут использоваться в десятке других проектов, то следовать всем описанным выше принципам вполне разумно. К повторно используемому коду предъявляются другие требования и при их проектировании нужно следовать другим принципам: простота в использовании и расширяемость таких классов значительно важнее стоимости сопровождения.
Если же вы проектируете классы бизнес-логики или простые библиотеки с ограниченным кругом пользователей, то можно не морочить себе голову с «канонами», а использовать упрощенную версию этого паттерна, которая работает только с управляемыми ресурсами.
(*) В С++, в отличие от C#, память тоже является ресурсом. Поэтому идиома RAII в языке С++ применяется как для освобождения динамически выделенной памяти, так и для освобождения любых других ресурсов, типа дескрипторов ОС или сокетов.
(**) В Java 7 наконец-то появилась конструкция, аналогичная конструкции using языка C#: try-with-resource statement
(***) К сожалению в языке C# для финализаторов выбран тот же самый синтаксис (тильда, за которой идет имя класса), который используется для деструкторов в языке С++. Но семантика деструктора и финализатора очень разная, поскольку деструктор подразумевает детерминированное освобождение ресурсов, а финализатор – нет.
(****) Здесь я, например, не говорил о том, как можно получить «утечку ресурсов» при появлении исключений или о проблемах с изменяемыми значимыми типами, реализующими интерфейс IDisposable. Об этом я уже писал ранее в заметках «Гарантии безопасности исключений» и «О вреде изменяемых значимых типов» соответственно.



