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

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

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


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

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


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


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

Обертки для неуправляемых ресурсов реализуется с помощью Janitor.Fody


Этот плагин к 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();
    }
}


Теперь можно перейти к самому распространенному случаю.

Подготовка


Для начала нам потребуется класс 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 надо делать вручную


Буду рад, если описанный способ пригодится читателям.

© Habrahabr.ru