Программист
0,0
рейтинг
17 ноября 2015 в 03:06

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


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

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

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

Предусловия


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


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

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

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


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

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

Буду рад, если описанный способ пригодится читателям.
@Bonart
карма
11,7
рейтинг 0,0
Программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (11)

  • +9
    А зачем тащить Fody в задачу, где достаточно базового класса? Не говоря уже о наличии таких готовых базовых классов в самом фреймворке.
    • +3
      Для меня проще использовать Fody, чем наследоваться от класса.
      Плюс Janitor.Fody сам расставляет проверки с выбросами ObjectDisposedException, что универсальный базовый класс сделать не может в принципе.
      А так на вкус и цвет все фломастеры разные.
      Я просто посчитал правильным сначала рассмотреть те варианты, которые моя реализация не покрывает.

    • 0
      Я после того как начал использовать costura.fody тащу её теперь в каждый проект
  • +6
    Если вы делаете обертку для неуправляемых ресурсов, то используйте SafeHandle и наследники.
    www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About
    • 0
      Да, вы абсолютно правы.
      Мне с неуправляемыми ресурсами приходилось сталкиваться крайне редко, и Janitor.Fody вполне хватало.
  • 0
    А если я не хочу Reactive Extensions тащить к себе в солюшен?
    • 0
      Напишите свои аналоги CompositeDisposable и Disposable.Create. Это несложно, благо Rx исходники никуда не прячет.
  • 0
    Сложность постобработки, в невозможности отладки такого кода, что часто критично.
    На мой взгляд проще и вернее использовать каноническую реализацию из MSDN, а все остальное может породить сложно диагностируемые проблемы…
    • 0
      В данном конкретном случае критично совсем не это, а главное свойство CriticalFinalizerObject:
      Гарантирует, что весь код завершения в производных классах помечен как критический

  • 0
    ну подход хорош тем, что вы вспомнили про SRP и выделили освобождение объекта в отдельную ответсвенность
    • 0
      На самом деле подход родился по ходу рефакторинга огромного количества легаси. Про SRP специально не думал, оно само получилось.

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