Wzorzec Dispose

Uwaga

Ta zawartość jest drukowana przez uprawnienie Pearson Education, Inc. z Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition. Wydanie to zostało opublikowane w 2008 roku, a książka została w pełni zmieniona w trzecim wydaniu. Niektóre informacje na tej stronie mogą być nieaktualne.

Wszystkie programy uzyskują co najmniej jeden zasób systemowy, taki jak pamięć, dojścia systemu lub połączenia bazy danych w trakcie wykonywania. Deweloperzy muszą zachować ostrożność podczas korzystania z takich zasobów systemowych, ponieważ muszą zostać wydane po ich uzyskaniu i użyciu.

ClR zapewnia obsługę automatycznego zarządzania pamięcią. Pamięć zarządzana (pamięć przydzielona przy użyciu operatora newjęzyka C#) nie musi być jawnie zwolniona. Jest on zwalniany automatycznie przez moduł odśmiecający śmieci (GC). To zwalnia deweloperów z żmudnego i trudnego zadania wydawania pamięci i był jednym z głównych powodów bezprecedensowej produktywności zapewnianej przez program .NET Framework.

Niestety pamięć zarządzana jest tylko jednym z wielu typów zasobów systemowych. Zasoby inne niż pamięć zarządzana muszą być wydawane jawnie i są określane jako zasoby niezarządzane. GC nie został specjalnie zaprojektowany do zarządzania takimi niezarządzanych zasobami, co oznacza, że odpowiedzialność za zarządzanie zasobami niezarządzanych leży w rękach deweloperów.

ClR zapewnia pewną pomoc w zwalnianiu niezarządzanych zasobów. System.Object deklaruje metodę Finalize wirtualną (nazywaną również finalizatorem), która jest wywoływana przez GC przed odzyskaniem pamięci obiektu przez GC i może zostać zastąpiona w celu zwolnienia niezarządzanych zasobów. Typy, które zastępują finalizator, są określane jako typy finalizowalne.

Chociaż finalizatory są skuteczne w niektórych scenariuszach oczyszczania, mają dwie znaczące wady:

  • Finalizator jest wywoływany, gdy GC wykryje, że obiekt kwalifikuje się do kolekcji. Dzieje się to w określonym czasie po tym, jak zasób nie jest już potrzebny. Opóźnienie między tym, kiedy deweloper może lub chce zwolnić zasób, a czasem, kiedy zasób jest rzeczywiście zwalniany przez finalizator, może być niedopuszczalny w programach, które uzyskują wiele ograniczonych zasobów (zasobów, które mogą być łatwo wyczerpane) lub w przypadkach, w których zasoby są kosztowne do utrzymania w użyciu (np. dużych buforów pamięci niezarządzanej).

  • Gdy CLR musi wywołać finalizator, musi odłożyć zbieranie pamięci obiektu do następnej rundy odzyskiwania pamięci (finalizatory działają między kolekcjami). Oznacza to, że pamięć obiektu (i wszystkie obiekty, do których się odwołuje) nie zostanie zwolniona przez dłuższy czas.

W związku z tym poleganie wyłącznie na finalizatorach może nie być odpowiednie w wielu scenariuszach, gdy ważne jest odzyskanie niezarządzanych zasobów tak szybko, jak to możliwe, w przypadku radzenia sobie z ograniczonymi zasobami lub w wysoce wydajnych scenariuszach, w których dodatkowe obciążenie GC finalizacji jest niedopuszczalne.

Struktura udostępnia System.IDisposable interfejs, który należy zaimplementować, aby zapewnić deweloperowi ręczny sposób wydawania niezarządzanych zasobów, gdy tylko nie są potrzebne. Udostępnia również metodę GC.SuppressFinalize , która może poinformować GC, że obiekt został ręcznie usunięty i nie musi być już sfinalizowany, w takim przypadku pamięć obiektu może zostać odzyskana wcześniej. Typy implementujące IDisposable interfejs są określane jako typy jednorazowe.

Wzorzec usuwania ma na celu standaryzację użycia i implementacji finalizatorów i interfejsu IDisposable .

Główną motywacją do wzorca jest zmniejszenie złożoności implementacji Finalize metod i Dispose . Złożoność wynika z faktu, że metody współdzielą niektóre, ale nie wszystkie ścieżki kodu (różnice zostały opisane w dalszej części rozdziału). Ponadto istnieją historyczne przyczyny niektórych elementów wzorca związanego z ewolucją obsługi języka na potrzeby deterministycznego zarządzania zasobami.

√ Zaimplementuj podstawowy wzorzec usuwania dla typów zawierających wystąpienia typów jednorazowych. Aby uzyskać szczegółowe informacje na temat podstawowego wzorca, zobacz sekcję Podstawowy wzorzec usuwania.

Jeśli typ jest odpowiedzialny za okres istnienia innych jednorazowych obiektów, deweloperzy również potrzebują sposobu ich usuwania. Użycie metody kontenera Dispose jest wygodnym sposobem, aby to możliwe.

√ Zaimplementuj podstawowy wzorzec usuwania i podaj finalizator typów zawierających zasoby, które muszą być zwolnione jawnie i które nie mają finalizatorów.

Na przykład wzorzec powinien być implementowany na typach przechowujących niezarządzane bufory pamięci. W sekcji Finalizable Types (Typy finalizowalne) omówiono wytyczne dotyczące implementowania finalizatorów.

√ ROZWAŻ zaimplementowanie podstawowego wzorca usuwania dla klas, które same nie przechowują niezarządzanych zasobów ani obiektów jednorazowych, ale mogą mieć podtypy, które to robią.

Doskonałym przykładem jest System.IO.Stream klasa . Chociaż jest to abstrakcyjna klasa bazowa, która nie przechowuje żadnych zasobów, większość jej podklas robi i z tego powodu implementuje ten wzorzec.

Podstawowy wzorzec usuwania

Podstawowa implementacja wzorca obejmuje zaimplementowanie interfejsu System.IDisposable i zadeklarowanie Dispose(bool) metody, która implementuje całą logikę oczyszczania zasobów, która ma być współdzielona między Dispose metodą i opcjonalnym finalizatorem.

Poniższy przykład przedstawia prostą implementację podstawowego wzorca:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

Parametr disposing logiczny wskazuje, czy metoda została wywołana z IDisposable.Dispose implementacji, czy z finalizatora. Implementacja Dispose(bool) powinna sprawdzić parametr przed uzyskaniem dostępu do innych obiektów referencyjnych (np. pole zasobu w poprzednim przykładzie). Dostęp do takich obiektów należy uzyskać tylko wtedy, gdy metoda jest wywoływana z implementacji IDisposable.Dispose (gdy disposing parametr jest równy true). Jeśli metoda jest wywoływana z finalizatora (disposing jest fałsz), inne obiekty nie powinny być dostępne. Przyczyną jest to, że obiekty są finalizowane w nieprzewidywalnej kolejności i dlatego, lub którekolwiek z ich zależności, mogły już zostać sfinalizowane.

Ponadto ta sekcja dotyczy klas z bazą, która nie implementuje jeszcze wzorca Dispose. Jeśli dziedziczysz z klasy, która już implementuje wzorzec, po prostu przesłoń metodę w celu zapewnienia dodatkowej Dispose(bool) logiki oczyszczania zasobów.

√ Deklaruj metodę, aby scentralizować całą logikę protected virtual void Dispose(bool disposing) związaną z wydawaniem niezarządzanych zasobów.

Wszystkie oczyszczanie zasobów powinno nastąpić w tej metodzie. Metoda jest wywoływana zarówno z finalizatora, jak IDisposable.Dispose i metody. Parametr będzie mieć wartość false, jeśli jest wywoływany z wewnątrz finalizatora. Należy go użyć, aby upewnić się, że żaden kod uruchomiony podczas finalizacji nie uzyskuje dostępu do innych obiektów, które można sfinalizować. Szczegóły implementacji finalizatorów opisano w następnej sekcji.

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

√ ZaimplementujIDisposable interfejs, wywołując po prostu polecenie Dispose(true)GC.SuppressFinalize(this).

Wywołanie polecenia powinno SuppressFinalize nastąpić tylko wtedy, gdy Dispose(true) zostanie wykonane pomyślnie.

public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
}

X NIE sprawiają, że metoda bez Dispose parametrów jest wirtualna.

Metoda Dispose(bool) jest tą, która powinna zostać zastąpiona przez podklasy.

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X NIE deklaruj żadnych przeciążeń Dispose metody innej niż Dispose() i Dispose(bool).

Dispose należy uznać za zastrzeżone słowo, aby ułatwić kodowanie tego wzorca i zapobiegać zamieszaniu między implementatorami, użytkownikami i kompilatorami. Niektóre języki mogą automatycznie implementować ten wzorzec dla niektórych typów.

√ Nie zezwalaj na wywoływanie Dispose(bool) metody więcej niż raz. Metoda może nie robić nic po pierwszym wywołaniu.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X UNIKAJ zgłaszania wyjątku z Dispose(bool) wyjątkiem sytuacji krytycznych, w których proces zawierający został uszkodzony (przecieki, niespójny stan udostępniony itp.).

Użytkownicy oczekują, że wywołanie Dispose metody nie zgłosi wyjątku.

Jeśli Dispose może zgłosić wyjątek, logika oczyszczania bloku na koniec nie zostanie wykonana. Aby obejść ten proces, użytkownik musi opakowować każde wywołanie Dispose (w bloku na koniec!) w bloku try, co prowadzi do bardzo złożonych procedur obsługi oczyszczania. Jeśli wykonujesz metodę Dispose(bool disposing) , nigdy nie zgłaszaj wyjątku, jeśli nie jest to fałsz. Spowoduje to zakończenie procesu, jeśli zostanie uruchomiony wewnątrz kontekstu finalizatora.

√ Zgłaszaj element ObjectDisposedException z dowolnego elementu członkowskiego, którego nie można użyć po usunięciu obiektu.

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

√ ROZWAŻ podanie metody Close(), oprócz Dispose()metody , jeśli close jest standardową terminologią w tym obszarze.

W takim przypadku ważne jest, aby implementacja Close była identyczna z Dispose implementacją i rozważyła jawne IDisposable.Dispose zaimplementowanie metody.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Typy finalizowalne

Typy finalizowalne to typy, które rozszerzają podstawowy wzorzec usuwania przez zastąpienie finalizatora i podanie ścieżki kodu finalizacji w metodzie Dispose(bool) .

Finalizatory są notorycznie trudne do zaimplementowania prawidłowo, przede wszystkim dlatego, że nie można podjąć pewnych (zwykle prawidłowych) założeń dotyczących stanu systemu podczas ich wykonywania. Należy wziąć pod uwagę następujące wytyczne.

Należy pamiętać, że niektóre wytyczne dotyczą nie tylko Finalize metody, ale także kodu wywoływanego z finalizatora. W przypadku wcześniej zdefiniowanego podstawowego wzorca usuwania oznacza to logikę wykonywaną wewnątrz Dispose(bool disposing) , gdy disposing parametr ma wartość false.

Jeśli klasa bazowa jest już sfinalizowana i implementuje podstawowy wzorzec usuwania, nie należy ponownie zastąpić Finalize . Zamiast tego należy po prostu zastąpić metodę , aby zapewnić dodatkową Dispose(bool) logikę oczyszczania zasobów.

Poniższy kod przedstawia przykład finalizowalnego typu:

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

X UNIKAJ tworzenia typów finalizowalnych.

Starannie zastanów się nad każdym przypadkiem, w którym uważasz, że potrzebny jest finalizator. Istnieje rzeczywisty koszt związany z wystąpieniami z finalizatorami, zarówno z punktu widzenia wydajności, jak i złożoności kodu. Preferuj używanie otoek zasobów, takich jak SafeHandle hermetyzowanie niezarządzanych zasobów tam, gdzie to możliwe, w takim przypadku finalizator staje się niepotrzebny, ponieważ otoka jest odpowiedzialna za własne oczyszczanie zasobów.

X NIE sprawia, że typy wartości można sfinalizować.

Tylko typy odwołań są rzeczywiście finalizowane przez CLR, a tym samym każda próba umieszczenia finalizatora na typie wartości zostanie zignorowana. Kompilatory języka C# i C++ wymuszają tę regułę.

• Zrobić typ finalizowalny, jeśli typ jest odpowiedzialny za zwolnienie niezarządzanego zasobu, który nie ma własnego finalizatora.

Podczas implementowania finalizatora wystarczy wywołać Dispose(false) i umieścić całą logikę oczyszczania zasobów wewnątrz Dispose(bool disposing) metody .

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

√ Zaimplementuj podstawowy wzorzec usuwania dla każdego typu finalizowalnego.

Daje to użytkownikom typu środki do jawnego przeprowadzenia deterministycznego czyszczenia tych samych zasobów, dla których finalizator jest odpowiedzialny.

X NIE uzyskuje dostępu do żadnych obiektów finalizowalnych w ścieżce kodu finalizatora, ponieważ istnieje znaczne ryzyko, że zostaną one już sfinalizowane.

Na przykład obiekt finalizowalny A, który ma odwołanie do innego obiektu finalizowalnego B nie może niezawodnie używać B w finalizatorze A lub na odwrót. Finalizatory są wywoływane w kolejności losowej (brakuje słabej gwarancji porządkowania na potrzeby finalizacji krytycznej).

Należy również pamiętać, że obiekty przechowywane w zmiennych statycznych będą zbierane w określonych punktach podczas zwalniania domeny aplikacji lub podczas zamykania procesu. Uzyskiwanie dostępu do zmiennej statycznej odwołującej się do obiektu finalizowalnego (lub wywoływania metody statycznej, która może używać wartości przechowywanych w zmiennych statycznych), może nie być bezpieczne, jeśli Environment.HasShutdownStarted zwraca wartość true.

√ Czy metoda jest chroniona Finalize .

Deweloperzy języka C#, C++i VB.NET nie muszą się tym martwić, ponieważ kompilatory pomagają wymusić te wytyczne.

X NIE zezwalaj wyjątkom na ucieczkę od logiki finalizatora, z wyjątkiem błędów krytycznych dla systemu.

Jeśli wyjątek zostanie zgłoszony z finalizatora, clR zamknie cały proces (w wersji .NET Framework w wersji 2.0), uniemożliwiając innym finalizatorom wykonywanie i wydawanie zasobów w kontrolowany sposób.

√ ROZWAŻ utworzenie i użycie obiektu finalizowalnego krytycznego (typu z hierarchią typów, która zawiera CriticalFinalizerObject) w sytuacjach, w których finalizator absolutnie musi być wykonywany nawet w obliczu wymuszonych zwolnień domeny aplikacji i przerwania wątku.

© Części 2005, 2009 Microsoft Corporation. Wszelkie prawa zastrzeżone.

Reprinted by permission of Pearson Education, Inc. from Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition by Krzysztof Cwalina and Brad Abrams, published oct 22, 2008 by Addison-Wesley Professional w ramach Microsoft Windows Development Series.

Zobacz też