Самая простая и надежная реализация шаблона проектирования 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 надо делать вручную

    Буду рад, если описанный способ пригодится читателям.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 13
    • +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 специально не думал, оно само получилось.
                • 0

                  А что делать с исключением в конструкторе?

                  • 0
                    То же что и всегда. Только в коде аварийной очистки будет ровно одна строка.

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