Pull to refresh

Самая простая и надежная реализация шаблона проектирования Dispose

Reading time 3 min
Views 19K

Казалось бы, данный шаблон не просто прост, а очень прост, подробно разобран не в одной известной книге.

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

Хочу поделиться своим способом реализации, который основан на минимизации изобретения велосипедов, максимальном уменьшении количества кода и увеличении его выразительности и прозрачности.

Предусловия


Никакого смешения управляемых и неуправляемых ресурсов


Я никогда не реализую сам и не советую коллегам использовать владение управляемыми и неуправляемыми ресурсами в одном классе.

Таким образом, один класс может:
  • Не владеть ресурсами вообще
  • Владеть одним неуправляемым ресурсом, то есть просто конвертировать его в управляемый
  • Владеть одним или многими управляемыми ресурсами

Наследование реализаций нежелательно


Я не использую наследование от классов без крайней необходимости, предлагаемая реализация предполагает в качестве объекта-владельца ресурсов экземпляр запечатанного класса.
Это не значит, что ее нельзя модифицировать для поддержки наследования.

Обертки для неуправляемых ресурсов реализуется с помощью 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 надо делать вручную

Буду рад, если описанный способ пригодится читателям.
Tags:
Hubs:
+13
Comments 13
Comments Comments 13

Articles