- PVSM.RU - https://www.pvsm.ru -
С этой статьей я начинаю публиковать целую серию статей, результатом которой будет книга по работе .NET CLR, и .NET в целом. Тема IDisposable была выбрана в качестве разгона, пробы пера. Вся книга будет доступна на GitHub: DotNetBook [1]. Так что Issues и Pull Requests приветствуются :)
Сейчас, наверное, практически любой программист, который разрабатывает на платформе .NET скажет что ничего проще этого паттерна нет. Что это известный из известнейших шаблонов, которые применяются на платформе. Однако даже в самой простой и известнейшей проблемной области всегда найдется второе дно, а за ним еще ряд скрытых кармашков, в которые вы никогда не заглядывали. Однако, как для тех кто смотрит тему впервые, так и для всех прочих (просто для того чтобы каждый из вас вспомнил основы (не пропускайте эти абзацы (я слежу!))) — опишем все от самого начала и до самого конца.
Если спросить, что такое IDisposable, вы наверняка ответите что это.
public interface IDisposable
{
void Dispose();
}
Для чего же создан интерфейс? Ведь если у нас есть умный Garbage Collector, который за нас чистит всю память, делает так чтобы мы вообще не задумывались о том, как чистить память, то становится не совсем понятно зачем ее вообще чистить. Однако есть нюансы. Существует некоторое заблуждение что IDisposable
сделан для того чтобы освобождать неуправляемые ресурсы. И это только часть правды. Чтобы единомоментно понять что это не так, достаточно вспомнить примеры неуправляемых ресурсов. Является ли неуправляемым класс File
? Нет. Может быть DbContext
? Опять же — нет. Неуправляемый ресурс — это то, что не входит в систему типов .NET. То, что не было создано платформой и находящееся вне ее скоупа. Простой пример — это дескриптор открытого файла в операционной системе. Дескриптор — это некоторое число, которое однозначно идентифицирует открытый операционной системой файл. Не вами, а именно операционной системой. Т.е. все управляющие структуры (такие как координаты файла на файловой системе, его фрагменты в случае фрагментации и прочая служебная информация, номера циллиндра, головки, сектора — в случае магнитного HDD) находятся не внутри платформы .NET а внутри ОС. И единственный неуправляемый ресурс, который уходит в платформу .NET является IntPtr число. Это число в свою очередь оборачивается FileSafeHandle, который в свою очередь оборачивается классом File. Т.е. класс File сам по себе неуправляемым ресурсом не является но аккумулирует в себе через дополнительную прослойку неуправляемый ресурс — дескриптор открытого файла — IntPtr. Как происходит чтение из такого файла? Через ряд методов WinAPI или ОС Linux.
Вторым примером неуправляемых ресурсов являются примитивы синхронизации в многопоточных и мультипроцессных программах. Такие как мьютексы, семафоры. Или же массивы данных, которые передаются через p/invoke.
Хорошо. С неуправляемыми ресурсами разобрались. Зачем же IDisposable в этих случаях? Затем что .NET Framework понятия не имеет о том, что происходит там, где его нет. Если вы открываете файл при помощи функций ОС, .NET ничего об этом не узнает. Если вы выделите участок памяти под собственные нужды (например, при помощи VirtualAlloc), .NET также ничего об этом не узнает. А если он ничего об этом не знает, он не освободит память, которая была занята вызовом VirtualAlloc. Или не закроет файл, открытый напрямую через вызов API ОС. Последствия этого могут быть совершенно разными и непредсказуемыми. Вы можете получть OutOfMemory если навыделяете слишком много памяти и не будете ее освобождать (а, например, по старой памяти будете просто обнулять указатель) либо заблокируете на долгое время файл на файловой шаре если он был открыт через средства ОС но не был закрыт. Пример с файловыми шарами особенно хорош потому что блокировка останется даже после закрытия приложения: открытость файла регулирет та сторона, на которой он находится. А удаленная сторона не получит сигнала закрытия файла если вы его не закрыли самостоятельно.
Во всех этих случаях необходим универсальный и узнаваемый протокол взаимодействия между системой типов и программистом, которая однозначно будет идентифицировать те типы, которые требуют принудительного закрытия. Этот протокол и есть интерфейс IDisposable. И звучит это примерно так: если тип содержит реализацию интерфейса IDisposable, то после того как вы закончите работу с его экземпляром, вы обязаны вызвать Dispose().
И ровно по этой причине есть два стандартных пути его вызова. Ведь как правило вы либо создаете экземпляр сущности чтобы быстренько с ней поработать в рамках одного метода либо в рамках времени жизни экземпляра своей сущности.
Первый вариант — это когда вы оборачиваете экземпляр в using(...){ ... }
. Т.е. вы прямо указываете что по окончании блока using объект должен быть уничтожен. Т.е. должен быть вызван Dispose(). Второй вариант — уничтожить его по окончании времени жизни объекта, который содержит ссылку на тот, который надо освободить. Но ведь в .NET кроме метода финализации нет ничего что намекало бы на автоматическое уничтожение объекта. Правильно? Но финализация нам совсем не подходит по той причине что она будет неизвестно когда вызвана. А нам надо освободать именно тогда, когда необходимо: сразу после того как нам более не нужен, например, открытый файл. Именно поэтому мы также должны реализовать IDisposable у себя и в методе Dispose вызвать Dispose у всех, кем мы владели чтобы освободить и их тоже. Таким образом мы соблюдаем протокол и это очень важно. Ведь если кто-то начал соблюдать некий протокол, его должны соблюдать все участники процесса: иначе будут проблемы.
Давайте пойдем в реализациях IDisposable от простого к сложному.
Первая и самая простая реализация которая только может прийти в голову — это просто вязть и реализовать IDisposable:
public class ResourceHolder : IDisposable
{
DisposableResource _anotherResource = new DisposableResource();
public void Dispose()
{
_anotherResource.Dispose();
}
}
Т.е. для начала мы создаем экземпляр некоторого ресурса, который должен быть освобожден и в методе Dispose() — освобождается.
Единственное чего здесь нет и что делает реализацию не консистентной, это возможность дальнейшей работы с экземпляром класса после его разрушения методом Dispose()
:
public class ResourceHolder : IDisposable
{
private DisposableResource _anotherResource = new DisposableResource();
private bool _disposed;
public void Dispose()
{
CheckDisposed();
_anotherResource.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckDisposed()
{
if(_disposed) {
throw new ObjectDisposedException();
}
}
}
Вызов CheckDisposed() необходимо вызывать первым выражением во всех публичных методах класса. Однако, если для разрушения управляемого ресурса, коим является DisposableResource
полученная структура класса ResourceHolder
выглядит нормально, то для случай инкапсулирования неуправляемого ресурса — нет.
Давайте придумаем вариант с неуправляемым ресурсом.
public class FileWrapper : IDisposable
{
IntPtr _handle;
public FileWrapper(string name)
{
_handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero);
}
public void Dispose()
{
CloseHandle(_handle);
}
[DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)]
private static extern IntPtr CreateFile(String lpFileName,
UInt32 dwDesiredAccess, UInt32 dwShareMode,
IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition,
UInt32 dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError=true)]
private static extern bool CloseHandle(IntPtr hObject);
}
Так какая разница в поведении двух последних примеров? В первом варианте у нас описано взаимодействие управляемого ресурса с другим управляемым. Это означает что в случае корректной работы программы ресурс будет освобожден в любом случае. Ведь DisposableResource
у нас — управляемый, а значит .NET CLR о нем прекрасно знает и в случае некорректного поведения — освободит из под него память. Заметьте, что я намеренно не делаю никаких предположений о том, что тип DisposableResource
инкапсулирует. Там может быть какая угодно логика и структура. Она может содержать как управляемые так и неуправляемые ресурсы. Нас это волновать не должно. Нас же не просят каждый раз декомпилировать чужие библиотеки и смотреть какие типы что используют: управляемые или неуправляемые ресурсы. А если наш тип использует неуправляемый ресурс, мы не можем этого не знать. Это мы делаем в классе FileWrapper
. Так что же произойдет в этом случае?
Если мы используем неуправляемые ресурсы, получается что у нас опять же два варианта: когда все хорошо и метод Dispose вызвался (тогда все хорошо :) ) и когда что-то случилось и метод Dispose отработать не смог. Сразу оговоримся, почему этого может не произойти:
using(obj) { ... }
, то во внутреннем блоке кода может возникнуть исключение, которое перехватывается блоком finally
, который нам не видно (это синтаксический сахар C#). В этом блоке неявно вызываетcя Dispose. Однако есть случаи, когда этого не происходит. Например, ThreadAbortException
, который не перехватывается ни catch
ни finally
. Это всегда надо учитывать. Ведь если у вас некий поток уйдет в подвисшее состояние и по книпке с GUI либо по HeartBeat таймеру вы его убьете, то те ресурсы, которые были захвачены и не были освобождены забудутся .NET'ом. Ведь он понятия не имеет как освобождать неуправляемые ресурсы: они повиснут в памяти до тех пор пока ОС не освободит их сама (например, при выходе из вашей программы. А иногда и неопределенное время уже после завершения работы приложения).Во всех таких случаях возникнет ситуация подвешенных в воздухе неуправляемых ресурсов. Ведь Garbage Collector понятия не имеет, что их надобно собрать. Максимум что он сделает — при очередном проходе поймет, что на граф объектов, содержащих наш объект типа FileWrapper
потеряна последняя ссылка и память перетрется теми объектами, на которые ссылки есть.
Как же защититься от подобного? Для этих случаев мы обязаны реализовать финализатор объекта. Финализатор не случайно имеет именно такое название. Это вовсе не деструктор, как может показаться изначально из-за схожести объявления финализаторов в C# и деструкторов — в C++. Финализатор в отличии от деструктора вызовется гарантированно тогда как деструктор может и не вызваться (ровно как и Dispose()
). Финализатор вызывается когда запускается Garbage Collection (пока этого знания достаточно, но по факту все несколько сложнее) и предназначен для гарантированного освобождения захваченных ресурсов если что-то пошло не так. И для случая освобождения неуправляемых ресурсов мы обязаны реализовывать финализатор. Также, повторюсь, из-за того что финализатор вызывается при запуске GC, в общем случае вы понятия не имеете, когда это произойдет.
Давайте расширим наш код:
public class FileWrapper : IDisposable
{
IntPtr _handle;
public FileWrapper(string name)
{
_handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero);
}
public void Dispose()
{
InternalDispose();
GC.SuppressFinalize(this);
}
private void InternalDispose()
{
CloseHandle(_handle);
}
~FileWrapper()
{
InternalDispose();
}
/// other methods
}
Мы усилили пример знаниями о процессе финализации и тем самым обезопасили приложение от потери информации о ресурсах если что-то пошло не так и Dispose() вызван не будет. Дополнительно, мы сделали вызов GC.SuppressFinalize для того чтобы отключить финализацию экземпляра типа если для него был вызван Dispose(). Нам же не надо дважды освобождать один и тот же ресурс? Также это стоит сделать по другой причине: мы снимаем нагрузку с очереди на финализацию, ускоряя случайный участок кода, в параллели с которым будет в случайном будущем отрабатывать финализация.
Теперь давайте еще усилим наш пример:
public class FileWrapper : IDisposable
{
IntPtr _handle;
bool _disposed;
public FileWrapper(string name)
{
_handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero);
}
public void Dispose()
{
CheckDisposed();
_disposed = true;
InternalDispose();
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckDisposed()
{
if(_disposed) {
throw new ObjectDisposedException();
}
}
private void InternalDispose()
{
CloseHandle(_handle);
}
~FileWrapper()
{
InternalDispose();
}
/// other methods
}
Теперь наш пример реализации типа, инкапсулирующего неуправляемый ресурс выглядит законченным. Повторный Dispose()
ошибочен и мы не даем его вызвать. Замечу что зачастую люди допускают повторный вызов Dispose()
для того чтобы избежаь мороки с вызывающим кодом. Однако это ломает целостность объекта. Если мы разрушили объект, значит с ним работать более нельзя. Это в свою очередь означает что мы обязаны вставлять вызов CheckDispose
в начало каждого публичного метода.
Однако в этом коде существует очень серъезная проблема, которая не даст ему работать так как задумали мы. Если мы повспонинаем, как отрабатывает процесс сборки мусора, то заметим одну деталь. При сборке мусора GC в первую очередь финализирует все, что напрямую унаследовано от Object, после чего принимается за те объекты, которые реализуют CriticalFinalizerObject. У нас же получается что оба класса, которые мы спроектировали наследуют Object: и это проблема. Мы понятия не имеем, в каком порядке мы уйдем на "последнюю милю". Тем не менее, более высокоуровневый объект может пытаться работать с объектом, который хранит неуправляемый ресурс — в своем финализаторе (хотя это уже звучит как плохая идея). Тут нам бы сильно пригодился порядок финализации. И для того чтобы его задать — мы должны унаследовать наш тип, инкапсулирующий unmanaged ресурс от CriticalFinalizerObject.
Вторая причина имеет более глубокие корни. Представьте себе что вы позволили себе написать приложение, которое не сильно заботится о памяти. Аллоцирует в огромных количествах без кэширования и прочих премудростей. Однажды такое приложение завалится с OutOfMemoryException. А когда приложение падает с этим исключением, возникают особые условия исполнения кода: ему нельзя что-либо пытаться аллоцировать. Ведь это приведет к повторному исключению, даже если предыдущее было поймано. Это вовсе не обозначает что мы не должны создавать новые экземпляры объектов. К этому исключению может привести обычный вызов метода. Например, вызов метода финаизации. Напомню, что метды компилируются тогда, когда они вызываются в первый раз. И это обычное поведение. Как же уберечься от этой проблемы? Достаточно легко. Если вы отнаследуете объект от CriticalFinalizerObject, то все методы этого типа будут компилироваться сразу же, при загрузке типа в память. Мало того, если вы пометите методы атрибутом [PrePrepareMethod], то они также будут предварительно скомпилированны и будут безопасными с точки зрения вызова при нехватке ресурсов.
Почему это так важно? Зачем тратить так много усилий на тех, кто уйдет в мир иной? А все дело в том что неуправляемые ресурсы могут повиснуть в системе очень надолго. Даже после того как ваше приложение завершит работу. Даже после перезагрузки компьютера (если пользователь открыл в вашем приложении файл с файловой шары, тот будет заблокирован удаленным хостом и отпущен либо по таймауту либо когда вы освободите ресурс, закрыв файл. Если ваше приложение вылетит в момент открытого файла, то он не будет закрыт даже после перезагрузки. Придется ждать достаточно продолжительное время для того чтобы удаленный хост отпустил бы его). Плюс ко всему вам нельзя допускать выброса исключений в финализаторах — это приведет к ускоренной гибели CLR и окончательному выбросу из приложения: вызовы финализаторов не оборачиваются try… catch. Т.е. освобождая ресурс вам надо быть уверенными в том что он еще может быть освобожден. И последний не менее интересный факт — если CLR осуществляет аварийную выгрузку домена, финализаторы типов, производных от CriticalFinalizerObject также будут вызваны в отличии от тех, кто наследовался напрямую от Object.
У меня есть некоторое ощущение что я для вас сейчас открою ящик Пандоры. Давайте поговорим про специальные типы: SafeHandle, CriticalHandle и их производные. И закончим уже, наконец, наш шаблон типа, предоставляющего доступ к unmanaged ресурсу. Но перед этим давайте попробуем перечислить все что к нам обычно идет из unmanaged мира:
SafeHandle — это специальный класс .NET CLR, который наследует CriticalFinalizerObject и который призван обернуть дескрипторы операционной системы максимально безопасно и удобно.
[SecurityCritical, SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
{
protected IntPtr handle; // Дескриптор, пришедший от ОС
private int _state; // Состояние (валидность, счетчик ссылок)
private bool _ownsHandle; // Флаг возможности освободить handle. Может так получиться что сы оборачиваем чужой handle и освобождать его не имеем права
private bool _fullyInitialized; // Экземпляр проинициализирован
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle)
{
}
// Финализатор по шаблону вызывает Dispose(false)
[SecuritySafeCritical]
~SafeHandle()
{
Dispose(false);
}
// Выставление hanlde может идти как вручную, так и при помощи p/invoke Marshal - автоматически
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
protected void SetHandle(IntPtr handle)
{
this.handle = handle;
}
// Метод необходим для того чтобы с IntPtr можно было бы работать напрямую. Используется
// для определения того, удалось ли создать дескриптор, сравнив его с одим из ранее
// определенных известных значений. Обратите внимание, что метод опасен по двум причинам:
// - Если дескриптор отмечен как недопустимый с помощью SetHandleasInvalid, DangerousGetHandle
// то все равно вернет исходное значение дескриптора.
// - Возвращенный дескриптор может быть переиспользован в любом месте. Это может как минимум
// означать что он без обратной связи перестанет работать. В худшем случае при прямой передаче
// IntPtr в другое место, он может уйти в ненадежный код и стать вектором атаки на приложение
// через подмену ресурса на одном IntPtr
[ResourceExposure(ResourceScope.None), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public IntPtr DangerousGetHandle()
{
return handle;
}
// Ресурс закрыт (более не доступен для работы)
public bool IsClosed {
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
get { return (_state & 1) == 1; }
}
// Ресурс не является доступным для работы. Вы можете переопределить свойство, изменив логику.
public abstract bool IsInvalid {
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
get;
}
// Закрытие ресурса через шаблон Close()
[SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public void Close() {
Dispose(true);
}
// Закрытие ресурса через шаблон Dispose()
[SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public void Dispose() {
Dispose(true);
}
[SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
protected virtual void Dispose(bool disposing)
{
// ...
}
// Вы должны вызывать этот метод всякий раз когда понимаете что handle более не является рабочим.
// Если вы этого не сделаете, можете получить утечку
[SecurityCritical, ResourceExposure(ResourceScope.None)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public extern void SetHandleAsInvalid();
// Переопределите данный метод чтобы указать каким образом необходимо освобождать
// ресурс. Необходимо быть крайне осторожным при написании кода, т.к. из него
// нельзя вызывать нескомпилированные методы, создавать новые объекты и бросать исключения.
// Возвращаемое значение - маркер успешности операции освобождения ресурсов.
// Причем если возвращаемое значение = false, будет брошено исключение
// SafeHandleCriticalFailure, которое в случае включенного SafeHandleCriticalFailure
// Managed Debugger Assistant войдет в точку останова.
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
protected abstract bool ReleaseHandle();
// Работа со счетчиком ссылок. Будет объяснено далее по тексту
[SecurityCritical, ResourceExposure(ResourceScope.None)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public extern void DangerousAddRef(ref bool success);
public extern void DangerousRelease();
}
Чтобы оценить полезность группы классов, производных от SafeHandle, достаточно вспомнить чем хороши все .NET типы: автоматизированностью уборки мусора. Т.о. оборачивая неуправляемый ресурс, SafeHandle наделяет его такими же свойствами, т.к. является управляемым. Плюс ко всему он содержит внутренний счетчик внешних ссылок, которые не могут быть учтены CLR. Т.е. ссылками из unsafe кода. Вручную увеличивать и уменьшать счетчик нет почти никакой необходимости: когда вы объявляете любой тип, производный от SafeHandle как параметр unsafe метода, то при входе в метод счетчик будет увеличен, а при выходе — уменьшен. Это свойство введено по той причине что когда вы перешли в unsafe код, передав туда дескриптор, то в другом потоке (если вы, конечно, работаете с одним дескриптором из нескольких потоков) обнулив ссылку на него, получите собранный SafeHandle. Со счетчиком же ссылок все проще: SafeHandle не будет собран пока дополнительно не обнулится счетчик. Вот почему вручную менять счетчик не стоит. Либо это надо делать очень аккуратно: возвращая его как только это становится возможным.
Второе назначение счетчика ссылок — это задание порядка финализации CriticalFinalizerObject
, которые друг на друга ссылаются. Если один SafeHandle-based тип ссылается на другой SafeHandle-based тип, то в конструкторе ссылающегося необходимо дополнительно увеличить счетчик ссылок, а в методе ReleaseHandle — уменьшить. Таким образом ваш объект не будет уничтожен пока не будет уничтожен тот, на который вы сослались. Однако чтобы не путаться, стоит избегать таких ситуаций.
Давайте напишем финальный вариант нашего класса, но теперь уже с последними знаниями о SafeHandlers:
public class FileWrapper : IDisposable
{
SafeFileHandle _handle;
bool _disposed;
public FileWrapper(string name)
{
_handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero);
}
public void Dispose()
{
CheckDisposed();
_disposed = true;
_handle.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckDisposed()
{
if(_disposed) {
throw new ObjectDisposedException();
}
}
[DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)]
private static extern SafeFileHandle CreateFile(String lpFileName,
UInt32 dwDesiredAccess, UInt32 dwShareMode,
IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition,
UInt32 dwFlagsAndAttributes,
IntPtr hTemplateFile);
/// other methods
}
Что его отличает? Зная, что если в DllImport методе в качестве возвращаемого значения установить любой SafeHandle-based тип, то Marshal его корректно создаст и проинициализирует, установив счетчик использований в 1, мы ставим тим SafeFileHandle в качестве возвращаемого для функции ядра CreateFile. Получив его, мы будем при вызове ReadFile и WriteFile использовать именно его (т.к. при вызове счетчик опять же увеличится, а при выходе — уменьшится, что даст нам гарантию существования handler на все время чтения и записи в файл). Тип этот спроектирован корректно, а это значит что он гарантированно закроет файловый дескриптор. Даже когда процесс аварийно завершит свою работу. А это значит что нам не надо реализовывать свой finalizer и все что с ним связано. Наш тип значительно упрощается.
Теперь поговорим про тонкий лед. В предыдущих частях рассказа об IDisposable мы проговорили одну очень важную концепцию, которая лежит не только в основе проектирования Disposable типов, но и в проектировании любого типа: концепция целостности объекта. Это значит что в любой момент времени объект находится в строго определенном состоянии и любое действие над ним переводит его состояние в одно из заранее определенных — при проектировании типа этого объекта. Другими словами — никакое действие над объектом не должно иметь возможность перевести его состояние в то, которое не было определено. Из этого вытекает проблема в спроектированных ранее типах: они не потокобезопаны. Есть потенциальная возможность вызова публичных методов этих типов в то время как идет разрушение объекта. Давайте решим эту проблему и решим стоит ли вообще ее решать
public class FileWrapper : IDisposable
{
IntPtr _handle;
bool _disposed;
object _disposingSync = new object();
public FileWrapper(string name)
{
_handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero);
}
public void Seek(int position)
{
lock(_disposingSync)
{
CheckDisposed();
// Seek API call
}
}
public void Dispose()
{
lock(_disposingSync)
{
CheckDisposed();
_disposed = true;
}
InternalDispose();
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckDisposed()
{
lock(_disposingSync)
{
if(_disposed) {
throw new ObjectDisposedException();
}
}
}
private void InternalDispose()
{
CloseHandle(_handle);
}
~FileWrapper()
{
InternalDispose();
}
/// other methods
}
Установка критической секции на код проверки _disposed
в Dispose() и по факту — установка критической секции на весь код публичных методов. Это решит нашу проблему одновременного входа в публичный метод экемпляра типа и в метод его разрушения, однако создаст таймер замедленного действия для ряда других проблем:
Второе, и на мой взгляд, самое важное. Мы допускаем ситуацию одновременного разрушения объекта с возможностью поработать с ним еще разок. На что мы вообще должны надеяться в данном случае? Что не выстрелит? Ведь если сначала отработает Dispose, то дальнейшее обращение с методам объекта обязано привести к ObjectDisposedException
. Отсюда возникает простой вывод: синхронизацию между вызовами Dispose() и остальными публичными методами типа необходимо делигировать обслуживающей стороне. Т.е. тому коду, который создал экземпляр класса FileWrapper
. Ведь только создающая сторона в курсе что она собирается делать с экземпларом класса и когда она собирается его разрушать.
Какой самый популярный шаблон реализации IDisposable
можно встретить в книгах по .NET разработке и во Всемирной Паутине? Какой шаблон ждут от вас люди в компаниях, когда вы идете собеседоваться на потенциально новое место работы? Вероятнее всего этот:
public class Disposable : IDisposable
{
bool _disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
CheckDisposed();
if(disposing)
{
// освобождаем управляемые ресурсы
}
// освобождаем неуправляемые ресурсы
}
protected void CheckDisposed()
{
if(_disposed)
{
throw new ObjectDisposedException();
}
}
~Disposable()
{
Dispose(false);
}
}
Что здесь не так и почему мы ранее в этой книге никогда так не писали? На самом деле шаблон хороший и без лишних слов охватывает все жизненные ситуации. Но его использование повсеместно как на мой взгляд не является правилом хорошего тона: ведь реальных неуправляемых ресурсов мы в практике почти никогда не видим и в этом случае пол-шаблона работает в холостую. Мало того он нарушает принцип разделения ответственности. Ведь он одновременно управляет и управляемыми ресурсами и неуправляемыми. На мой скромный взгляд это совершенно не правильно. Давайте взглянем на несколько иной подход. Disposable Design Principle. Если коротко, то суть в следующем:
Disposing разделяется на два уровня классов:
Именно поэтому я с самого начала ввел разделение на два типа: на содержащий управляемый ресурс и содержащий неуправляемый ресурс. Они должны работать совершенно по-разному.
Итак, мы узнали много нового про этот простейший шаблон. Давайте определим его плюсы:
Минусов шаблона я вижу намного больше чем плюсов:
IEnumerator<T>
тянет за собой IDisposable
?IDisposable
через explicit реализацию. Или получить тип, реализующий IDisposable без возможности определить, кто его должен разрушать. Сторона, которая выдала или вы сами; virtual void Dispose()
метод для переопределения, но это не решит других проблем, связанных с шаблоном;Dispose()
как правило идет в конце файла, тогда как сtor
объявляется в начале. При модификации класса и вводе новых ресурсов можно легко ошибиться и забыть зарегистрировать disposing для них. Lifetime
, речь о котором пойдет в следующей части.GitHub: .NET Book [1]
Автор: sidristij
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/267704
Ссылки в тексте:
[1] DotNetBook: https://github.com/mumusan/dotnetbook
[2] Источник: https://habrahabr.ru/post/341864/
Нажмите здесь для печати.