Udostępnij za pośrednictwem


Wzorzec usuwania

Uwaga / Notatka

Ta treść jest przedrukowana za zgodą Pearson Education, Inc. z Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2. wydanie. 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ęć, uchwyty systemowe lub połączenia z bazą danych w trakcie działania. 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ł zaprojektowany specjalnie do zarządzania takimi niezarządzanymi zasobami, co oznacza, że odpowiedzialność za zarządzanie niezarządzanymi zasobami leży w rękach programistó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 pamięci niezarządzanej).

  • Kiedy CLR musi wywołać finalizator, musi odłożyć odzyskiwanie pamięci obiektu do następnego cyklu odzyskiwania pamięci (finalizatory działają między cyklami). Oznacza to, że pamięć obiektu (oraz wszystkich obiektów, na które się odwołuje) nie zostanie zwolniona przez dłuższy okres czasu.

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 Dispose ma na celu standaryzację użycia i implementacji finalizatorów i interfejsu IDisposable.

Główną motywacją tego wzorca jest zmniejszenie złożoności implementacji metod Finalize 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 Dispose kontenera jest wygodnym sposobem na osiągnięcie tego celu.

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 . Pomimo że jest to abstrakcyjna klasa bazowa, która nie przechowuje żadnych zasobów, większość jej podklas robi to, dlatego 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 typu logicznego disposing wskazuje, czy metoda została wywołana z implementacji IDisposable.Dispose, 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 false), inne obiekty nie powinny być używane. Przyczyną jest to, że obiekty są sfinalizowane w nieprzewidywalnej kolejności, więc one 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 po klasie, która już implementuje wzorzec, po prostu przesłoń metodę Dispose(bool), aby zapewnić dodatkową logikę czyszczenia 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 i z metody IDisposable.Dispose. 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();
    }
}

Zaimplementuj IDisposable interfejs, po prostu wywołując Dispose(true), a następnie GC.SuppressFinalize(this).

Wywołanie SuppressFinalize powinno 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ń dla metody Dispose, poza 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.

✓ Zezwalaj na wywoływanie Dispose(bool) metody więcej niż raz. Metoda może zdecydować się 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, Dispose(bool) chyba że w sytuacjach krytycznych, w których proces został poważnie uszkodzony (przecieki, niespójny stan współdzielony itp.).

Użytkownicy oczekują, że wywołanie Dispose 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 musiałby opakowywać każde wywołanie Dispose (w bloku finally!) 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 disposing jest fałszywy. 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Ż dostarczenie metody Close(), oprócz Dispose(), jeśli zamknięcie jest standardową terminologią w tym obszarze.

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

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ż finalizowalna i implementuje podstawowy wzorzec zarządzania zasobami, nie należy ponownie zastępować Finalize. Zamiast tego należy po prostu zastąpić metodę Dispose(bool) , aby zapewnić dodatkową 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 możliwych do finalizacji.

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 opakowań zasobów, takich jak SafeHandle, do hermetyzowania niezarządzanych zasobów tam, gdzie to możliwe, w takim przypadku finalizator staje się niepotrzebny, ponieważ opakowanie jest odpowiedzialne za własne oczyszczanie zasobów.

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

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

✓ ZRÓB typ finalizowalny, jeśli jest on 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) {
        ...
    }
}

Stosuj 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.

✓ Zadbaj, aby metoda była 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 krytycznego obiektu finalizowalnego (typu z hierarchią typów, która zawiera CriticalFinalizerObject) w sytuacjach, gdy finalizator musi zostać bezwzględnie wykonany, nawet w przypadku wymuszonego odciążenia domen aplikacji i zakończenia wątku.

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

Przedrukowane za zgodą Pearson Education, Inc. z Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition przez Krzysztofa Cwalinę i Brada Abramsa, opublikowane 22 października 2008 przez Addison-Wesley Professional w ramach serii Microsoft Windows Development.

Zobacz także