Implementowanie IDisposable

Nieraz zdarza się, że musimy samodzielnie napisać jakąś klasę implementującą interfejs IDisposable. Wszyscy na pewno znają wzorzec implementowania Dispose. Jednak moim zdaniem ma on kilka wad:

  • Nie jest bezpieczny wątkowo
  • Daje możliwość wielokrotnego sprzątania zasobów – nie zabezpiecza klienta przed wielokrotnym wywołaniem Dispose
  • Jak chcemy mieć finalizator to musimy go sobie sami napisać i wywołać w nim Dispose(false)
  • Nieszczęsna metoda Dispose(bool disposing) sprowadzającego się do tego, że 90% zasobów zwalniamy w bloku „if (disposing)”, a konstrukcja „if (!disposing)” jest w ogóle pozbawiona jakiekolwiek sensu – ogólnie moim zdaniem ten sposób implementacji jest bardzo nieczytelny. Jedną jego zaletą jest możliwość decydowania o dokładnej kolejności zwalania zasobów

Z tego względu proponuję następującą bazową implementację IDisposable:

    public class Disposable : IDisposable
    {
        private const int DisposedFlag = 1;

        private readonly StackTrace creationStackTrace;

        private int _isDisposed;

        public Disposable(bool withFinalizer = false)
        {
#if DEBUG
            creationStackTrace = new StackTrace();
#else
            if (!withFinalizer)
            {
                GC.SuppressFinalize(this);
            }
#endif
        }

        ~Disposable()
        {
            DisposeUnmanaged();
#if DEBUG
            Debug.Fail(GetType() + " in not disposed" + Environment.NewLine + creationStackTrace);
#endif
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Dispose is implemented correctly")]
        public void Dispose()
        {
            int wasDisposed = Interlocked.Exchange(ref _isDisposed, DisposedFlag);
            if (wasDisposed != DisposedFlag)
            {
                DisposeResources();
            }
        }

        protected virtual void DisposeManaged()
        {
        }

        protected virtual void DisposeUnmanaged()
        {
        }

        protected void RequireNotDisposed()
        {
            if (IsDisposed)
            {
                string typeName = GetType().FullName;
                throw new ObjectDisposedException(typeName);
            }
        }

        public bool IsDisposed
        {
            get
            {
                Thread.MemoryBarrier();
                return _isDisposed == DisposedFlag;
            }
        }

        private void DisposeResources()
        {
            DisposeManaged();
            DisposeUnmanaged();
            GC.SuppressFinalize(this);
        }
    }

Zalety klasy Disposable:

  • Bezpieczna wątkowo
  • Zabezpieczenie przed wielokrotnym zwalnianiem zasobów
  • Posiada pomocniczą metodę RequireNotDisposed, którą można wykorzystać w metodach publicznych klasy
  • W konfiguracji Debug zawsze tworzony jest finalizator, który może być użyty podczas śledzenia, czy czasami nie zapomnieliśmy wywołać Dispose
  • Jak mamy zasoby zarządzane, to przeciążamy metodę DisposeManaged. Z kolei w przypadku zasobów niezarządzanych – DisposeUnmanaged
  • Możliwość sprawdzenia czy obiekt ma już zwolnione zasoby poprzez IsDisposed
  • W razie potrzeby posiadania finalizatora wystarczy wykorzystać konstruktor z parameterem (wiem, że bool jest brzydki – można zrobić enuma)

Na marginesie jakby ktoś był zmuszony dziedziczyć (sic!) po klasie dziedziczącej po Disposable to radzę to robić w taki sposób – link. Dzięki temu nie musimy się martwić, że ktoś zapomni wywołać base.Dispose(disposing) albo base.DisposeManagedResources.

Jeżeli, ktoś nie chce albo nie może wykorzystywać dziedziczenia, to proponuję wykorzystać wzorzec delegacji w połączeniu z następującą klasą:

    public class Disposer : Disposable
    {
        private readonly Action action;

        public Disposer(Action action)
        {
            if (action == null) throw new ArgumentNullException("action");
            this.action = action;
        }

        protected override void DisposeManaged()
        {
            action();
        }
    }

Powyższa konstrukcja oparta o delegat jest poprawna tylko w przypadku zasobów zarządzanych. Nie można wykorzystać podobnego mechanizmu dla DisposeUnmanaged. Obiekt Action jest zarządzany i może dojść do zwolnienia jego zasobów zanim wywoła się finalizator obiektu, który posiada do niego referencję.

5 myśli nt. „Implementowanie IDisposable”

  1. Implementacja finalizatora klasy Finalizer nie jest poprawna, bowiem zawiera odwołanie do zarządzanego zasobu, jakim jest instancja delegata onFinalize (a następujące po nim wywołanie GC.SuppressFinalize jest zbędne). Istnieje sposób, by to obejść i przy okazji zupełnie wyeliminować klasę Finalizer, wystarczy przemienić metodę Disposable.OnFinalize w finalizator, a warunek w konstruktorze zmodyfikować do postaci:

    if (!withFinalizer)
    {
        GC.SuppressFinalize(this);
    }
    

    Świetny pomysł z użyciem Debug.Fail w finalizatorze klas oczekujących jawnego wywołania Dispose.

    1. Dziękuję za komentarz.

      Ad. 1. Ale my nie zwalniamy delegata tylko go wywołujemy. Sęk w tym, że delegat powinien zawierać operacje potrzebne do zwolnienia zasobów niezarządzanych (dlatego w w Disposable przekazywana jest metoda DisposeUnmanaged jako delegat).

      Ad. 2. Zgadza się – kiedyś implementacja tak wyglądała, ale miała one swoje wady. Jak obiekt ma finalizator, to od razu podczas tworzenia wchodzi do kolejki obiektów do finalizacji przez co mamy overhead na usuwanie obiektu z tej kolejki.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *


cztery × 6 =

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>