Казалось бы, данный шаблон не просто прост, а очень прост, подробно разобран не в одной известной книге.
Тем не менее, до сих пор даже в рамках одного проекта он зачастую может быть реализован по-разному, создавая зоопарк из велосипедов, костылей и утечек.
Хочу поделиться своим способом реализации, который основан на минимизации изобретения велосипедов, максимальном уменьшении количества кода и увеличении его выразительности и прозрачности.
Предусловия
Никакого смешения управляемых и неуправляемых ресурсов
Я никогда не реализую сам и не советую коллегам использовать владение управляемыми и неуправляемыми ресурсами в одном классе.
Таким образом, один класс может:
- Не владеть ресурсами вообще
- Владеть одним неуправляемым ресурсом, то есть просто конвертировать его в управляемый
- Владеть одним или многими управляемыми ресурсами
Наследование реализаций нежелательно
Я не использую наследование от классов без крайней необходимости, предлагаемая реализация предполагает в качестве объекта-владельца ресурсов экземпляр запечатанного класса.
Это не значит, что ее нельзя модифицировать для поддержки наследования.
Обертки для неуправляемых ресурсов реализуется с помощью Janitor.Fody
Обновление: в комментариях совершенно справедливо указано, что с этой целью лучше использовать классы, унаследованные от CriticalFinalizerObject и SafeHandle.
Ваш код (пример из документации):
public class Sample : IDisposable
{
IntPtr handle;
public Sample()
{
handle = new IntPtr();
}
public void Method()
{
//Some code
}
public void Dispose()
{
//must be empty
}
void DisposeUnmanaged()
{
CloseHandle(handle);
handle = IntPtr.Zero;
}
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool CloseHandle(IntPtr hObject);
}
Результат постобработки:
public class Sample : IDisposable
{
IntPtr handle;
volatile int disposeSignaled;
bool disposed;
public Sample()
{
handle = new IntPtr();
}
void DisposeUnmanaged()
{
CloseHandle(handle);
handle = IntPtr.Zero;
}
[DllImport("kernel32.dll", SetLastError = true)]
static extern Boolean CloseHandle(IntPtr handle);
public void Method()
{
ThrowIfDisposed();
//Some code
}
void ThrowIfDisposed()
{
if (disposed)
{
throw new ObjectDisposedException("TemplateClass");
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref disposeSignaled, 1) != 0)
{
return;
}
DisposeUnmanaged();
GC.SuppressFinalize(this);
disposed = true;
}
~Sample()
{
Dispose();
}
}
Теперь можно перейти к самому распространенному случаю, ради которого и была написана эта статья.
Реализация шаблона проектирования Dispose для управляемых ресурсов
Подготовка
Для начала нам потребуется класс CompositeDisposable из библиотеки Reactive Extensions.
К нему необходимо дописать небольшой метод расширения:
public static void Add(this CompositeDisposable litetime, Action action)
{
lifetime.Add(Disposable.Create(action));
}
Добавление поля, отвечающего за очистку
private readonly CompositeDisposable lifetime = new CompositeDisposable();
Реализация метода Dispose
public void Dispose()
{
lifetime.Dispose();
}
Больше ничего и никогда в этот метод добавлять не нужно.
Очистка явно конструируемых ресурсов
Достаточно просто добавить простейший код прямо в место выделения ресурса.
Было:
myOwnResourceField = new Resource();
// И где-то при очистке
if (myOwnResourceField != null)
{
myOwnResourceField.Dispose();
myOwnResourceField = null;
}
Стало:
lifetime.Add(myOwnedResourceField = new Resource());
Отписка от событий
Было:
sender.Event += Handler;
// И где-то при очистке
sender.Event -= Handler
Стало:
sender.Event += Handler;
lifetime.Add(() => sender.Event -= Handler);
Отписка от IObservable
Было:
subscription = observable.Subscribe(Handler);
// И где-то при очистке
if (subscription != null)
{
subscription.Dispose();
subscription = null;
}
Стало:
lifetime.Add(observable.Subscribe(Handler));
Выполнение произвольных действий при очистке
CreateAction();
lifetime.Add(() => DisposeAction());
Проверка состояния объекта
if (lifetime.IsDisposed)
Выводы
Предлагаемый способ:
- универсален: гарантированно покрываются любые управляемые ресурсы, даже такие как «при очистке выполните следующий код»
- выразителен: дополнительный код невелик по объему
- привычен: используется обыкновенный класс из очень популярной библиотеки, который, вдобавок, при необходимости несложно написать и самостоятельно
- прозрачен: код очистки каждого ресурса расположен вплотную к коду захвата, большинство потенциальных утечек будут сразу замечены при рецензировании
- ухудшает производительность: добавляет «memory traffic» за счет создания новых объектов
- не влияет на безопасность использования уже «мертвого» объекта: собственные ресурсы очистятся только однажды, но любые проверки с выбросом ObjectDisposedException надо делать вручную
Буду рад, если описанный способ пригодится читателям.
Комментарии (11)
gandjustas
17.11.2015 12:42+6Если вы делаете обертку для неуправляемых ресурсов, то используйте SafeHandle и наследники.
www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-AboutBonart
17.11.2015 13:18Да, вы абсолютно правы.
Мне с неуправляемыми ресурсами приходилось сталкиваться крайне редко, и Janitor.Fody вполне хватало.
EngineerSpock
17.11.2015 16:47А если я не хочу Reactive Extensions тащить к себе в солюшен?
Bonart
17.11.2015 16:51Напишите свои аналоги CompositeDisposable и Disposable.Create. Это несложно, благо Rx исходники никуда не прячет.
Viacheslav01
17.11.2015 22:56Сложность постобработки, в невозможности отладки такого кода, что часто критично.
На мой взгляд проще и вернее использовать каноническую реализацию из MSDN, а все остальное может породить сложно диагностируемые проблемы…Bonart
18.11.2015 08:21В данном конкретном случае критично совсем не это, а главное свойство CriticalFinalizerObject:
Гарантирует, что весь код завершения в производных классах помечен как критический
kekekeks
А зачем тащить Fody в задачу, где достаточно базового класса? Не говоря уже о наличии таких готовых базовых классов в самом фреймворке.
Bonart
Для меня проще использовать Fody, чем наследоваться от класса.
Плюс Janitor.Fody сам расставляет проверки с выбросами ObjectDisposedException, что универсальный базовый класс сделать не может в принципе.
А так на вкус и цвет все фломастеры разные.
Я просто посчитал правильным сначала рассмотреть те варианты, которые моя реализация не покрывает.
IamKarlson
Я после того как начал использовать costura.fody тащу её теперь в каждый проект