Казалось бы, данный шаблон не просто прост, а очень прост, подробно разобран не в одной известной книге.
Тем не менее, до сих пор даже в рамках одного проекта он зачастую может быть реализован по-разному, создавая зоопарк из велосипедов, костылей и утечек.
Хочу поделиться своим способом реализации, который основан на минимизации изобретения велосипедов, максимальном уменьшении количества кода и увеличении его выразительности и прозрачности.
Предусловия
Никакого смешения управляемых и неуправляемых ресурсов
Я никогда не реализую сам и не советую коллегам использовать владение управляемыми и неуправляемыми ресурсами в одном классе.
Таким образом, один класс может:
- Не владеть ресурсами вообще
- Владеть одним неуправляемым ресурсом, то есть просто конвертировать его в управляемый
- Владеть одним или многими управляемыми ресурсами
Наследование реализаций нежелательно
Я не использую наследование от классов без крайней необходимости, предлагаемая реализация предполагает в качестве объекта-владельца ресурсов экземпляр запечатанного класса.
Это не значит, что ее нельзя модифицировать для поддержки наследования.
Обертки для неуправляемых ресурсов реализуется с помощью Janitor.Fody
Обновление: в комментариях совершенно справедливо указано, что с этой целью лучше использовать классы, унаследованные от CriticalFinalizerObject и SafeHandle.
То, чем пользовался я
Этот плагин к Fody — бесплатному инструменту модификации кода сборок после компиляции — позволит не выписывать вручную тысячу и одну деталь реализации, необходимой для корректного освобождения ресурсов.
Ваш код (пример из документации):
Результат постобработки:
Ваш код (пример из документации):
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 надо делать вручную
Буду рад, если описанный способ пригодится читателям.