Właściwe użycie interfejsu IDisposable


1657

Wiem z lektury dokumentacji Microsoft, że „podstawowe” użycieIDisposable interfejsu jest czyszczenie niezarządzanych zasobów.

Dla mnie „niezarządzany” oznacza takie rzeczy, jak połączenia z bazą danych, gniazda, uchwyty okien itp. Ale widziałem kod, w którym Dispose()metoda jest zaimplementowana w celu swobodnego zarządzania zasobów, co wydaje mi się zbędne, ponieważ śmieciarz powinien zająć To dla ciebie.

Na przykład:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }

Moje pytanie brzmi: czy to powoduje, że pamięć śmieciarza jest wolna zużyta MyCollectionszybciej niż zwykle?

edycja : Do tej pory ludzie opublikowali kilka dobrych przykładów użycia IDisposable do czyszczenia niezarządzanych zasobów, takich jak połączenia z bazą danych i mapy bitowe. Ale załóżmy, że _theListw powyższym kodzie zawarte miliona ciągi i chciał, aby zwolnić pamięć, że teraz , zamiast czekać na śmieciarza. Czy powyższy kod by to osiągnął?


34
Podoba mi się zaakceptowana odpowiedź, ponieważ zawiera prawidłowy „wzorzec” używania IDisposable, ale jak powiedział OP w swojej edycji, nie odpowiada na zamierzone pytanie. IDisposable nie „wywołuje” GC, po prostu „oznacza” obiekt jako możliwy do zniszczenia. Ale jaki jest prawdziwy sposób na uwolnienie pamięci „teraz” zamiast oczekiwania na uruchomienie GC? Myślę, że to pytanie zasługuje na więcej dyskusji.
Punit Vora

40
IDisposablenic nie zaznacza. DisposeMetoda robi to, co musi zrobić, aby oczyścić zasoby używane przez instancji. To nie ma nic wspólnego z GC.
John Saunders

4
@Jan. Rozumiem IDisposable. I dlatego powiedziałem, że zaakceptowana odpowiedź nie odpowiada zamierzonemu pytaniu PO (i dalszej edycji), czy IDisposable pomoże w <i> uwolnieniu pamięci </i>. Ponieważ IDisposablenie ma to nic wspólnego z uwalnianiem pamięci, tylko zasoby, to jak już powiedziałeś, nie ma potrzeby ustawiania zarządzanych referencji na zero, co OP robił w swoim przykładzie. Tak więc poprawna odpowiedź na jego pytanie brzmi: „Nie, to nie pomaga zwolnić pamięci szybciej. W rzeczywistości nie pomaga w ogóle zwolnić pamięci, tylko zasoby”. Ale i tak dziękuję za Twój wkład.
Punit Vora,

9
@desigeek: jeśli tak jest, to nie powinieneś mówić, że „IDisposable nie„ wywołuje ”GC, tylko„ oznacza ”obiekt jako możliwy do zniszczenia”
John Saunders,

5
@desigeek: Nie ma gwarantowanego sposobu deterministycznego zwolnienia pamięci. Możesz wywołać GC.Collect (), ale jest to uprzejme żądanie, a nie żądanie. Wszystkie działające wątki muszą zostać zawieszone, aby można było kontynuować wyrzucanie elementów bezużytecznych - zapoznaj się z koncepcją punktów bezpieczeństwa .NET, jeśli chcesz dowiedzieć się więcej, np. Msdn.microsoft.com/en-us/library/678ysw69(v=vs.110). aspx . Jeśli wątek nie może zostać zawieszony, np. Z powodu wywołania niezarządzanego kodu, GC.Collect () może nic nie zrobić.
Beton Głuptak

Odpowiedzi:


2608

Celem Dispose jest uwolnienie niezarządzanych zasobów. W pewnym momencie należy to zrobić, w przeciwnym razie nigdy nie zostaną oczyszczone. Śmieciarka nie wie, jak wywołać DeleteHandle()zmienną typu IntPtr, nie wie, czy musi zadzwonić DeleteHandle().

Uwaga : Co to jest niezarządzany zasób ? Jeśli znalazłeś go w Microsoft .NET Framework: jest zarządzany. Jeśli sam zacząłeś grzebać w MSDN, jest to niezarządzane. Wszystko, czego używałeś wywołań P / Invoke, aby wyjść poza przyjemny, wygodny świat wszystkiego, co jest dostępne w .NET Framework, jest niezarządzane - a teraz jesteś odpowiedzialny za oczyszczenie go.

Obiekt, który utworzyłeś, musi ujawnić jakąś metodę, którą świat zewnętrzny może wywołać, aby oczyścić niezarządzane zasoby. Metodę można nazwać dowolnie:

public void Cleanup()

lub

public void Shutdown()

Zamiast tego istnieje znormalizowana nazwa tej metody:

public void Dispose()

Utworzono nawet interfejs IDisposable, który ma tylko jedną metodę:

public interface IDisposable
{
   void Dispose()
}

Sprawiasz, że obiekt ujawnia IDisposableinterfejs, i w ten sposób obiecujesz, że napisałeś tę pojedynczą metodę, aby oczyścić niezarządzane zasoby:

public void Dispose()
{
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}

I jesteś skończony. Tyle że możesz zrobić lepiej.


Co się stanie, jeśli twój obiekt przydzieli 250 MB System.Drawing.Bitmap (tj. Zarządzaną klasą .NET Bitmap) jako pewnego rodzaju bufor ramki? Jasne, jest to zarządzany obiekt .NET, a śmieciarz go zwolni. Ale czy naprawdę chcesz zostawić 250 MB pamięci po prostu tam siedząc - czekając, aż śmieciarz w końcu przyjdzie i ją zwolni? Co się stanie, jeśli istnieje otwarte połączenie z bazą danych ? Z pewnością nie chcemy, aby to połączenie było otwarte i czekało, aż GC sfinalizuje obiekt.

Jeśli użytkownik zadzwonił Dispose()(co oznacza, że ​​nie planuje już używać obiektu), dlaczego nie pozbyć się marnotrawstwa map bitowych i połączeń z bazą danych?

Więc teraz będziemy:

  • pozbyć się niezarządzanych zasobów (ponieważ musimy), i
  • pozbyć się zarządzanych zasobów (ponieważ chcemy być pomocni)

Zaktualizujmy więc naszą Dispose()metodę, aby pozbyć się zarządzanych obiektów:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}

I wszystko jest dobrze, tyle że możesz zrobić lepiej !


Co się stanie, jeśli osoba zapomni zadzwonić Dispose()na Twój obiekt? Wtedy wyciekną niektóre niezarządzane zasoby!

Uwaga: nie będą przeciekać zarządzanych zasobów, ponieważ w końcu uruchomi się moduł wyrzucania elementów bezużytecznych w wątku w tle i zwolnią pamięć związaną z wszelkimi nieużywanymi obiektami. Będzie to obejmowało Twój obiekt i wszelkie zarządzane obiekty, których używasz (np. BitmapI DbConnection).

Jeśli osoba zapomniała zadzwonić Dispose(), nadal możemy uratować jej bekon! Mamy jeszcze sposób nazwać to dla nich kiedy garbage collector wreszcie trafia dokoła do uwolnienia (tj finalizowanie) Naszym celem.

Uwaga: Śmieciarka ostatecznie uwolni wszystkie zarządzane obiekty. Kiedy to robi, wywołuje Finalize metodę na obiekcie. GC nie wie ani nie dba o twoją metodę Dispose . To była tylko nazwa, którą wybraliśmy dla metody, którą nazywamy, gdy chcemy pozbyć się niezarządzanych rzeczy.

Zniszczenie naszego obiektu przez śmieciarza to idealny czas na uwolnienie tych nieznośnych niezarządzanych zasobów. Robimy to, zastępując Finalize()metodę.

Uwaga: W języku C # nie jawnie zastępujesz Finalize()metody. Napisać metodę, która wygląda jak danego C ++ destruktora i kompilatora zatrzymuje się swoją wdrożenie Finalize()metody:

~MyObject()
{
    //we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
    Dispose(); //<--Warning: subtle bug! Keep reading!
}

Ale w tym kodzie jest błąd. Widzisz, śmieciarz działa na wątku w tle ; nie znasz kolejności niszczenia dwóch obiektów. Jest całkiem możliwe, że w twoim Dispose()kodzie nie ma już obiektu zarządzanego , którego próbujesz się pozbyć (ponieważ chciałeś być pomocny):

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
      this.frameBufferImage = null;
   }
}

Potrzebny jest zatem sposób, Finalize()aby powiedzieć, Dispose()że nie powinien on dotykać żadnych zarządzanych zasobów (ponieważ mogą ich nie być już ), jednocześnie uwalniając niezarządzane zasoby.

Standardowym wzorcem do tego jest posiadanie Finalize()i Dispose()wywoływanie trzeciej (!) Metody; gdzie przekazujesz logiczne powiedzenie, jeśli dzwonisz z Dispose()(w przeciwieństwie do Finalize()), co oznacza, że ​​można bezpiecznie uwolnić zarządzane zasoby.

Ta wewnętrzna metoda może mieć dowolną nazwę, taką jak „CoreDispose” lub „MyInternalDispose”, ale tradycja nazywa ją Dispose(Boolean):

protected void Dispose(Boolean disposing)

Jednak bardziej pomocną nazwą parametru może być:

protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too, but only if I'm being called from Dispose
   //(If I'm being called from Finalize then the objects might not exist
   //anymore
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}

I zmieniasz implementację IDisposable.Dispose()metody na:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
}

a twój finalizator:

~MyObject()
{
   Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}

Uwaga : Jeśli Twój obiekt pochodzi z obiektu, który implementuje Dispose, nie zapomnij wywołać swojej podstawowej metody Dispose po zastąpieniu Dispose:

public override void Dispose()
{
    try
    {
        Dispose(true); //true: safe to free managed resources
    }
    finally
    {
        base.Dispose();
    }
}

I wszystko jest dobrze, tyle że możesz zrobić lepiej !


Jeśli użytkownik wywoła Dispose()Twój obiekt, wszystko zostało wyczyszczone. Później, kiedy pojawia się moduł wyrzucający śmieci i wywołuje Finalize, zadzwoni Disposeponownie.

Jest to nie tylko marnotrawstwo, ale jeśli Twój obiekt zawiera śmieciowe odniesienia do obiektów, które zostały już usunięte po ostatnim wywołaniu Dispose(), spróbujesz usunąć je ponownie!

Zauważysz w moim kodzie, że starałem się usunąć odwołania do obiektów, które pozbyłem się, więc nie próbuję wywoływać Dispose do śmieci. Ale to nie powstrzymało przedostania się subtelnego robaka.

Gdy użytkownik wywołuje Dispose(): uchwyt CursorFileBitmapIconServiceHandle jest zniszczony. Później, gdy moduł wyrzucający śmieci uruchomi się, spróbuje ponownie zniszczyć ten sam uchwyt.

protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy 
   ...
}

Sposób, w jaki to naprawiasz, polega na powiedzeniu śmieciarzowi, że nie musi zawracać sobie głowy finalizowaniem obiektu - jego zasoby zostały już wyczyszczone i nie trzeba więcej pracować. Można to zrobić poprzez wywołanie GC.SuppressFinalize()w Dispose()metodzie:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
   GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}

Teraz, gdy użytkownik zadzwonił Dispose(), mamy:

  • uwolniono niezarządzane zasoby
  • uwolnione zasoby zarządzane

Nie ma sensu, aby GC obsługiwał finalizator - wszystko załatwione.

Czy nie mogę użyć Finalize do czyszczenia niezarządzanych zasobów?

Dokumentacja Object.Finalizemówi:

Metoda Finalize służy do wykonywania operacji czyszczenia niezarządzanych zasobów przechowywanych przez bieżący obiekt przed zniszczeniem obiektu.

Ale dokumentacja MSDN mówi również IDisposable.Dispose:

Wykonuje zadania zdefiniowane przez aplikację związane z uwalnianiem, zwalnianiem lub resetowaniem niezarządzanych zasobów.

Więc co to jest? Które jest miejscem, w którym mogę uporządkować niezarządzane zasoby? Odpowiedź to:

To Twój wybór! Ale wybierz Dispose.

Z pewnością możesz umieścić swoje niezarządzane porządki w finalizatorze:

~MyObject()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //A C# destructor automatically calls the destructor of its base class.
}

Problem polega na tym, że nie masz pojęcia, kiedy śmieciarz podejdzie do sfinalizowania obiektu. Twoje un zarządzane, UN, UN potrzebne wykorzystywane rodzime zasoby będą trzymać się aż do garbage collector ostatecznie skończy. Następnie wywoła metodę finalizatora; czyszczenie niezarządzanych zasobów. Dokumentacja Object.Finalize wskazuje na to:

Dokładny czas wykonania finalizatora jest nieokreślony. Aby zapewnić deterministyczne uwalnianie zasobów dla instancji klasy, zaimplementuj metodę Close lub zapewnij IDisposable.Disposeimplementację.

Jest to zaleta używania Disposedo czyszczenia niezarządzanych zasobów; poznajesz i kontrolujesz, kiedy niezarządzane zasoby są usuwane. Ich zniszczenie jest „deterministyczne” .


Aby odpowiedzieć na twoje pierwotne pytanie: dlaczego nie zwolnić pamięci teraz, zamiast na to, kiedy GC zdecyduje się to zrobić? Mam twarzy oprogramowania do rozpoznawania, że potrzeby , aby pozbyć się z 530 MB wewnętrznej obrazami teraz , ponieważ nie jesteś już potrzebny. Kiedy tego nie robimy: maszyna zgrzyta do zatrzymania.

Czytanie bonusowe

Dla każdego, kto lubi styl tej odpowiedzi (wyjaśniając dlaczego , więc jak staje się oczywiste), sugeruję przeczytanie pierwszego rozdziału Essential COM Dona Boxa:

Na 35 stronach wyjaśnia problemy związane z używaniem obiektów binarnych i wymyśla COM na twoich oczach. Kiedy zrozumiesz, dlaczego COM, pozostałe 300 stron jest oczywistych i opisuje szczegółowo implementację Microsoftu.

Myślę, że każdy programista, który kiedykolwiek miał do czynienia z obiektami lub COM, powinien przynajmniej przeczytać pierwszy rozdział. To najlepsze wytłumaczenie czegokolwiek w historii.

Dodatkowe czytanie bonusowe

Kiedy wszystko, co wiesz, jest złe przez Erica Lipperta

Dlatego naprawdę bardzo trudno jest napisać poprawny finalizator, a najlepszą radą, jaką mogę ci dać, jest nie próbować .


12
Możesz zrobić lepiej - musisz dodać wywołanie do GC.SuppressFinalize () w Dispose.
cokół

55
@Daniel Earwicker: To prawda. Microsoft chciałby, abyś przestał całkowicie używać Win32 i trzymał się ładnie abstrakcyjnych, przenośnych, niezależnych od urządzenia wywołań .NET Framework. Jeśli chcesz przeszukać system operacyjny poniżej; ponieważ myślisz, że wiesz, na czym działa system operacyjny: bierzesz życie w swoje ręce. Nie każda aplikacja .NET działa w systemie Windows lub na komputerze.
Ian Boyd

34
To świetna odpowiedź, ale myślę, że przydałaby się jednak ostateczna lista kodów dla standardowego przypadku i przypadku, w którym klasa wywodzi się z klasy podstawowej, która już implementuje Dispose. np. po przeczytaniu tutaj ( msdn.microsoft.com/en-us/library/aa720161%28v=vs.71%29.aspx ), a także pomyliłem się co do tego, co powinienem zrobić, wychodząc z klasy, która już implementuje Dispose ( hej, jestem nowy w tym).
integra753,

5
@GregS i inni: Ogólnie nie zawracałbym sobie głowy ustawianiem odniesień do null. Po pierwsze oznacza to, że nie możesz ich wykonać readonly, a po drugie, musisz zrobić bardzo brzydkie !=nullkontrole (jak w przykładowym kodzie). Możesz mieć flagę disposed, ale łatwiej się tym nie przejmować. .NET GC jest na tyle agresywny, że odniesienie do pola xnie będzie już liczone jako „używane” do momentu przekroczenia x.Dispose()linii.
fragmenty

7
Na drugiej stronie książki Dona Boxa, o której wspomniałeś, wykorzystuje on przykład implementacji algorytmu wyszukiwania O (1), którego „szczegóły pozostają jako ćwiczenie dla czytelnika”. Śmiałem się.
wytrzyj

65

IDisposablejest często używany do wykorzystania usinginstrukcji i skorzystania z łatwego sposobu przeprowadzenia deterministycznego czyszczenia zarządzanych obiektów.

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

6
Osobiście podoba mi się to, ale tak naprawdę nie zakochuje się w wytycznych dotyczących projektowania ram.
mqp

4
Uznałbym to za właściwy projekt, ponieważ umożliwia łatwe deterministyczne zakresy i konstrukcje / porządki zakresów, szczególnie w połączeniu z obsługą wyjątków, blokowaniem i blokami użytkowania zasobów niezarządzanych w złożony sposób. Język oferuje to jako pierwszorzędną funkcję.
yfeldblum,

Nie jest on dokładnie zgodny z regułami określonymi w FDG, ale z pewnością jest prawidłowym użyciem wzorca, ponieważ jest wymagany w celu użycia przez „instrukcję using”.
Scott Dorman

2
Dopóki Log.Outdent nie rzuca, zdecydowanie nie ma w tym nic złego.
Daniel Earwicker

1
Różne odpowiedzi na pytanie Czy niewłaściwe jest używanie IDisposable i „używanie” jako sposobu na uzyskanie „zachowania w zakresie” dla bezpieczeństwa wyjątkowego? przejdź do bardziej szczegółowych informacji na temat tego, dlaczego różni ludzie lubią tę technikę. To jest nieco kontrowersyjne.
Brian

44

Wzorzec Dispose ma na celu zapewnienie mechanizmu czyszczenia zasobów zarządzanych i niezarządzanych, a kiedy to nastąpi, zależy od tego, jak wywoływana jest metoda Dispose. W twoim przykładzie użycie Dispose w rzeczywistości nie robi nic związanego z usuwaniem, ponieważ wyczyszczenie listy nie ma wpływu na usuwanie tej kolekcji. Podobnie, wezwania do ustawienia zmiennych na null również nie mają wpływu na GC.

Możesz zapoznać się z tym artykułem, aby uzyskać więcej informacji na temat wdrażania wzorca Dispose, ale w zasadzie wygląda to tak:

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

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

Najważniejszą metodą jest tutaj Dispose (bool), który faktycznie działa w dwóch różnych okolicznościach:

  • disposing == true: metoda została wywołana bezpośrednio lub pośrednio przez kod użytkownika. Zarządzane i niezarządzane zasoby mogą być usuwane.
  • disposing == false: metoda została wywołana przez środowisko wykonawcze z wnętrza finalizatora i nie powinieneś odwoływać się do innych obiektów. Można usuwać tylko niezarządzane zasoby.

Problem z po prostu pozwoleniem GC na wykonanie czyszczenia polega na tym, że nie masz rzeczywistej kontroli nad tym, kiedy GC uruchomi cykl zbierania (możesz wywołać GC.Collect (), ale tak naprawdę nie powinieneś), aby zasoby mogły pozostać około dłużej niż potrzeba. Pamiętaj, że wywołanie Dispose () tak naprawdę nie powoduje cyklu gromadzenia ani w żaden sposób nie powoduje, że GC zbiera / uwalnia obiekt; po prostu zapewnia środki do bardziej deterministycznego czyszczenia używanych zasobów i informuje GC, że to czyszczenie zostało już wykonane.

Cały sens IDisposable i wzorca usuwania nie polega na natychmiastowym uwolnieniu pamięci. Jedynym przypadkiem, gdy wezwanie do Dispose będzie miało nawet szansę natychmiastowego zwolnienia pamięci, jest wtedy, gdy obsługuje on == fałszywy scenariusz i manipuluje niezarządzanymi zasobami. W przypadku kodu zarządzanego pamięć nie zostanie faktycznie odzyskana, dopóki GC nie uruchomi cyklu gromadzenia, nad którym tak naprawdę nie masz kontroli (poza wywoływaniem GC.Collect (), o którym już wspomniałem, nie jest dobrym pomysłem).

Twój scenariusz nie jest tak naprawdę prawidłowy, ponieważ ciągi w .NET nie używają żadnych niezmienionych zasobów i nie implementują IDisposable, nie ma sposobu, aby zmusić je do „wyczyszczenia”.


4
Nie zapomniałeś wdrożyć finalizatora?
Budda

@Budda: Nie, używa SafeHandle. Nie potrzebujesz niszczyciela.
Henk Holterman,

9
+1 za dodanie siatki bezpieczeństwa dla wielu połączeń do Dispose (). Specyfikacja mówi, że wiele połączeń powinno być bezpieczne. Zbyt wiele klas Microsoft nie może tego zaimplementować i pojawia się irytujący wyjątek ObjectDisposedException.
Jesse Chisholm

5
Ale Dispose (bool disposing) jest twoją własną metodą w klasie SimpleCleanup i nigdy nie będzie wywoływana przez framework. Ponieważ wywołujesz go tylko z parametrem „prawda”, „usuwanie” nigdy nie będzie fałszywe. Twój kod jest bardzo podobny do przykładu MSDN dla IDisposable, ale brakuje mu finalizatora, jak wskazał @Budda, z którego pochodzi wywołanie z disposing = false.
yoyo

19

Po wywołaniu Dispose nie powinno być żadnych dalszych wywołań metod obiektu (chociaż obiekt powinien tolerować dalsze wywołania Dispose). Dlatego przykład w pytaniu jest głupi. Jeśli wywołane zostanie Dispose, sam obiekt można odrzucić. Dlatego użytkownik powinien po prostu odrzucić wszystkie odwołania do tego całego obiektu (ustawić je na wartość NULL), a wszystkie powiązane z nim obiekty automatycznie zostaną oczyszczone.

Jeśli chodzi o ogólne pytanie dotyczące zarządzanego / niezarządzanego i dyskusję w innych odpowiedziach, myślę, że każda odpowiedź na to pytanie musi zaczynać się od definicji niezarządzanego zasobu.

Sprowadza się do tego, że istnieje funkcja, którą można wywołać, aby wprowadzić system w stan, a także inną funkcję, którą można wywołać, aby przywrócić go z tego stanu. Teraz, w typowym przykładzie, pierwszy może być funkcją, która zwraca uchwyt pliku, a drugi może być wywołaniem CloseHandle.

Ale - i to jest klucz - mogą to być dowolne pasujące pary funkcji. Jeden buduje stan, drugi go niszczy. Jeśli stan został zbudowany, ale jeszcze nie zburzony, istnieje instancja zasobu. Musisz zorganizować porzucenie we właściwym czasie - zasoby nie są zarządzane przez CLR. Jedynym automatycznie zarządzanym typem zasobu jest pamięć. Istnieją dwa rodzaje: GC i stos. Rodzaje wartości są zarządzane przez stos (lub poprzez zaczepienie przejażdżki wewnątrz typów referencyjnych), a typy referencyjne są zarządzane przez GC.

Funkcje te mogą powodować zmiany stanu, które można dowolnie przeplatać lub mogą wymagać perfekcyjnego zagnieżdżenia. Zmiany stanu mogą być bezpieczne dla wątków lub nie.

Spójrz na przykład w pytaniu Sprawiedliwości. Zmiany w wcięciach pliku dziennika muszą być idealnie zagnieżdżone, w przeciwnym razie wszystko pójdzie nie tak. Nie są też prawdopodobnie bezpieczne dla wątków.

Możliwe jest połączenie się ze śmieciarzem, aby oczyścić niezarządzane zasoby. Ale tylko wtedy, gdy funkcje zmiany stanu są bezpieczne dla wątków, a dwa stany mogą mieć okresy życia, które nakładają się w jakikolwiek sposób. Zatem przykład zasobu Sprawiedliwości NIE może mieć finalizatora! To po prostu nikomu nie pomogłoby.

W przypadku tego rodzaju zasobów można po prostu wdrożyć IDisposable, bez finalizatora. Finalizator jest absolutnie opcjonalny - tak musi być. Zostało to zlekceważone lub nawet nie wspomniane w wielu książkach.

Następnie musisz użyć tego usingoświadczenia, aby mieć szansę na sprawdzenie, czy Disposezostanie wywołane. Zasadniczo jest to podobne do zaczepienia się na stosie (tak jak finalizator do GC, usingdo stosu).

Brakująca część polega na tym, że musisz ręcznie napisać Dispose i wywołać ją na swoje pola i klasę podstawową. Programiści C ++ / CLI nie muszą tego robić. Kompilator zapisuje je w większości przypadków.

Istnieje alternatywa, którą preferuję dla stanów, które idealnie się zagnieżdżają i nie są bezpieczne dla wątków (oprócz wszystkiego innego, unikanie IDisposable pozwala uniknąć kłótni z kimś, kto nie może się oprzeć dodaniu finalizatora do każdej klasy, która implementuje IDisposable) .

Zamiast pisać klasę, piszesz funkcję. Funkcja akceptuje uczestnika, aby oddzwonił do:

public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

A potem prosty przykład to:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

Przekazana lambda służy jako blok kodu, więc to tak, jakbyś stworzył własną strukturę kontrolną, która służy temu samemu celowi using, z tym wyjątkiem, że nie istnieje już ryzyko, że osoba dzwoniąca nadużyje jej. Nie ma mowy, żeby nie udało się wyczyścić zasobu.

Ta technika jest mniej przydatna, jeśli zasób może mieć nakładające się okresy istnienia, ponieważ wtedy chcesz być w stanie zbudować zasób A, następnie zasób B, następnie zabić zasób A, a następnie zabić zasób B. jeśli zmusiłeś użytkownika do idealnego zagnieżdżenia w ten sposób. Ale musisz użyć IDisposable(ale nadal bez finalizatora, chyba że wdrożyłeś wątkowe bezpieczeństwo, które nie jest darmowe).


re: „Po wywołaniu Dispose nie powinno być żadnych wywołań metod obiektu. „Powinno” być słowem operatywnym. Jeśli oczekujesz na działania asynchroniczne, mogą one pojawić się po usunięciu obiektu. Powodowanie wyjątku ObjectDisposedException.
Jesse Chisholm

Twoja wydaje się być jedyną odpowiedzią inną niż moja, która dotyczy idei, że niezarządzane zasoby obejmują stan, którego GC nie rozumie. Kluczowym aspektem niezarządzanego zasobu jest jednak to, że jeden lub więcej podmiotów, których stan może wymagać wyczyszczenia, może nadal istnieć, nawet jeśli obiekt, który „jest właścicielem” zasobu, nie istnieje. Jak ci się podoba moja definicja? Całkiem podobne, ale myślę, że sprawia, że ​​„zasób” jest nieco bardziej rzeczownikowy (jest to „zgoda” zewnętrznego obiektu na zmianę jego zachowania w zamian za powiadomienie, kiedy jego usługi nie są już potrzebne)
supercat

@ supercat - jeśli jesteś zainteresowany, napisałem następujący post kilka dni po tym, jak napisałem powyższą odpowiedź: zapachegantcode.wordpress.com/2009/02/13/…
Daniel Earwicker,

1
@DanielEarwicker: Ciekawy artykuł, chociaż mogę wymyślić co najmniej jeden rodzaj niezarządzanego zasobu, którego tak naprawdę nie obejmujesz: subskrypcje wydarzeń z obiektów długowiecznych. Subskrypcje zdarzeń są zamienne, ale nawet gdyby pamięć była nieograniczona, ich usunięcie może być kosztowne. Na przykład moduł wyliczający dla kolekcji, który pozwala na modyfikację podczas wyliczania, może wymagać subskrypcji, aby zaktualizować powiadomienia z kolekcji, a kolekcja może być aktualizowana wiele razy w ciągu swojego życia. Jeśli
wyliczacze

1
Para operacji enteri exitjest rdzeniem tego, jak myślę o zasobie. Subskrybowanie / wypisywanie się z wydarzeń powinno się z tym zmieścić bez trudności. Pod względem cech ortogonalnych / zamiennych jest praktycznie nie do odróżnienia od wycieku pamięci. (Nie jest to zaskakujące, ponieważ subskrypcja po prostu dodaje obiekty do listy.)
Daniel Earwicker

17

Scenariusze Korzystam z IDisposable: oczyszczaj niezarządzane zasoby, anuluj subskrypcję zdarzeń, zamykaj połączenia

Idiom, którego używam do implementacji IDisposable ( nie jest wątkowo bezpieczny ):

class MyClass : IDisposable {
    // ...

    #region IDisposable Members and Helpers
    private bool disposed = false;

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

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                // cleanup code goes here
            }
            disposed = true;
        }
    }

    ~MyClass() {
        Dispose(false);
    }
    #endregion
}

Pełne wyjaśnienie wzoru można znaleźć na stronie msdn.microsoft.com/en-us/library/b1yfkh5e.aspx
LicenseQ

3
nigdy nie powinien zawierać finalizatora, chyba że masz niezarządzane zasoby. Nawet wtedy preferowaną implementacją jest zawinięcie niezarządzanego zasobu w SafeHandle.
Dave Black

11

Tak, ten kod jest całkowicie zbędny i niepotrzebny i nie zmusza śmieciarza do robienia czegokolwiek, czego inaczej by nie zrobił (gdy wystąpienie MyCollection wykracza poza zakres, to znaczy.) Zwłaszcza .Clear()wywołania.

Odpowiedz na swoją edycję: Sortuj. Jeśli to zrobię:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has no Dispose() method
    instance.FillItWithAMillionStrings();
}

// 1 million strings are in memory, but marked for reclamation by the GC

Jest funkcjonalnie identyczny z tym dla celów zarządzania pamięcią:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has your Dispose()
    instance.FillItWithAMillionStrings();
    instance.Dispose();
}

// 1 million strings are in memory, but marked for reclamation by the GC

Jeśli naprawdę naprawdę chcesz natychmiast zwolnić pamięć, zadzwoń GC.Collect(). Jednak nie ma powodu, aby to robić. Pamięć zostanie zwolniona, gdy będzie potrzebna.


2
re: „Pamięć zostanie zwolniona, gdy będzie potrzebna”. Zamiast tego powiedz: „kiedy GC zdecyduje, że jest to potrzebne”. Możesz zobaczyć problemy z wydajnością systemu, zanim GC zdecyduje, że pamięć jest naprawdę potrzebna. Uwolnienie go teraz może nie być konieczne, ale może być przydatne.
Jesse Chisholm

1
Istnieje kilka narożnych przypadków, w których anulowanie odwołań w kolekcji może przyspieszyć odśmiecanie elementów wymienionych w ten sposób. Na przykład, jeśli tworzona jest duża tablica i zawiera odniesienia do mniejszych nowo utworzonych elementów, ale nie jest to potrzebne przez bardzo długi czas, porzucenie tablicy może spowodować, że te przedmioty będą przechowywane do następnego poziomu 2 GC, podczas zerowania w pierwszej kolejności przedmioty mogą kwalifikować się do następnego poziomu 0 lub poziomu 1 GC. Oczywiście, posiadanie dużych krótkotrwałych obiektów na
stosie

1
... zerując takie tablice przed ich porzuceniem, czasami zmniejszam wpływ GC.
supercat

11

Jeśli MyCollectioni tak będzie zbierany śmieci, nie powinieneś go usuwać. Spowoduje to po prostu wyrzucenie procesora więcej niż to konieczne, a nawet może unieważnić niektóre wstępnie obliczone analizy, które moduł śmieciowy już wykonał.

Zwykle IDisposablerobię takie rzeczy, jak zapewnienie prawidłowego usuwania wątków wraz z niezarządzanymi zasobami.

EDYCJA W odpowiedzi na komentarz Scotta:

Jedyny czas, na jaki wpływa to na parametry wydajności GC, to wywołanie [sic] GC.Collect () „

Koncepcyjnie, GC utrzymuje widok wykresu odniesienia do obiektu i wszystkich odniesień do niego z ramek stosu wątków. Ta sterta może być dość duża i obejmować wiele stron pamięci. Jako optymalizacja, GC buforuje swoją analizę stron, które prawdopodobnie nie zmienią się bardzo często, aby uniknąć niepotrzebnego ponownego skanowania strony. GC otrzymuje powiadomienie z jądra, gdy dane na stronie się zmieniają, więc wie, że strona jest brudna i wymaga ponownego skanowania. Jeśli kolekcja znajduje się w Gen0, prawdopodobnie inne rzeczy na stronie również się zmieniają, ale jest to mniej prawdopodobne w Gen1 i Gen2. Anegdotycznie te haki nie były dostępne w Mac OS X dla zespołu, który przeniósł GC na Maca, aby wtyczka Silverlight działała na tej platformie.

Kolejny punkt przeciwko niepotrzebnemu usuwaniu zasobów: wyobraź sobie sytuację, w której proces jest rozładowywany. Wyobraź sobie również, że proces działa już od pewnego czasu. Istnieje prawdopodobieństwo, że wiele stron pamięci tego procesu zostało zamienionych na dysk. Przynajmniej nie są już w pamięci podręcznej L1 lub L2. W takiej sytuacji nie ma sensu, aby aplikacja, która się rozładowuje, zamieniała wszystkie dane i strony kodowe z powrotem do pamięci w celu „zwolnienia” zasobów, które zostaną zwolnione przez system operacyjny i tak po zakończeniu procesu. Dotyczy to zarządzanych, a nawet niektórych niezarządzanych zasobów. Tylko zasoby, które utrzymują przy życiu wątki inne niż tło, muszą zostać usunięte, w przeciwnym razie proces pozostanie przy życiu.

Teraz podczas normalnego wykonywania istnieją efemeryczne zasoby, które należy poprawnie wyczyścić (ponieważ @ fezmonkey wskazuje połączenia z bazą danych, gniazda, uchwyty okien ), aby uniknąć wycieków pamięci niezarządzanych. To są rzeczy, które należy usunąć. Jeśli utworzysz jakąś klasę, która posiada wątek (a przez własność rozumiem, że go utworzyła i dlatego jest odpowiedzialna za zapewnienie, że przestanie, przynajmniej przez mój styl kodowania), wtedy ta klasa najprawdopodobniej musi zaimplementować IDisposablei zerwać wątek podczas Dispose.

.NET Framework wykorzystuje IDisposableinterfejs jako sygnał, a nawet ostrzeżenie dla programistów, że ta klasa musi zostać usunięta. Nie mogę wymyślić żadnych typów w ramach, które implementują IDisposable(z wyjątkiem jawnych implementacji interfejsu), w których usuwanie jest opcjonalne.


Wezwanie Dispose jest całkowicie ważne, legalne i zachęcane. Obiekty, które implementują IDisposable, zwykle robią to z jakiegoś powodu. Jedyny czas, na jaki wpływa to na parametry wydajności GC, to wywołanie funkcji GC.Collect ().
Scott Dorman,

Dla wielu klas .net usuwanie jest „nieco” opcjonalne, co oznacza, że ​​porzucenie instancji „zwykle” nie spowoduje żadnych problemów, o ile nie oszaleje się tworząc nowe instancje i porzucając je. Na przykład kod generowany przez kompilator formantów wydaje się tworzyć czcionki, gdy formanty są tworzone, i porzucać je, gdy formularze są usuwane; jeśli ktoś utworzy i zbywa tysiące kontrolek, może to wiązać tysiące uchwytów GDI, ale w większości przypadków kontrolki nie są tak tworzone i niszczone. Niemniej jednak należy starać się unikać takiego porzucenia.
supercat

1
Podejrzewam, że w przypadku czcionek problem polega na tym, że Microsoft nigdy tak naprawdę nie zdefiniował, który podmiot jest odpowiedzialny za usunięcie obiektu „czcionki” przypisanego do kontrolki; w niektórych przypadkach formanty mogą współdzielić czcionkę z obiektem o dłuższym czasie życia, więc posiadanie formantu Usuń czcionkę byłoby złe. W innych przypadkach czcionka zostanie przypisana do formantu i nigdzie indziej, więc jeśli formant nie usunie go, nikt tego nie zrobi. Nawiasem mówiąc, tej trudności z czcionkami można by uniknąć, gdyby istniała oddzielna klasa jednorazowego FontTemplate, ponieważ formanty nie wydają się używać uchwytu GDI ich Font.
supercat

Na temat Dispose()połączeń opcjonalnych patrz: stackoverflow.com/questions/913228/...
RJ Cuthbertson

7

W opublikowanym przykładzie nadal nie „zwalnia ona teraz pamięci”. Cała pamięć jest gromadzona w pamięci, ale może umożliwić gromadzenie pamięci we wcześniejszej generacji . Aby się upewnić, musiałbyś przeprowadzić kilka testów.


Wytyczne dotyczące projektowania ram są wytycznymi, a nie regułami. Mówią, do czego służy interfejs, kiedy go używać, jak go używać, a kiedy go nie używać.

Kiedyś czytałem kod, który był prostym RollBack () w przypadku niepowodzenia przy użyciu IDisposable. Poniższa klasa MiniTx sprawdziłaby flagę na Dispose (), a gdyby Commitwywołanie nigdy się nie wydarzyło, wywołałoby Rollbacksię samo. Dodano warstwę pośrednią, dzięki czemu kod wywołujący jest znacznie łatwiejszy do zrozumienia i utrzymania. Wynik wyglądał mniej więcej tak:

using( MiniTx tx = new MiniTx() )
{
    // code that might not work.

    tx.Commit();
} 

Widziałem też kod czasowy / rejestrujący, który robi to samo. W tym przypadku metoda Dispose () zatrzymała stoper i zarejestrowała wyjście z bloku.

using( LogTimer log = new LogTimer("MyCategory", "Some message") )
{
    // code to time...
}

Oto kilka konkretnych przykładów, które nie wykonują żadnego niezarządzanego czyszczenia zasobów, ale z powodzeniem używają IDisposable do tworzenia czystszego kodu.


Spójrz na przykład @Daniel Earwicker przy użyciu funkcji wyższego rzędu. Do testów porównawczych, pomiaru czasu, rejestrowania itp. Wydaje się to o wiele prostsze.
Aluan Haddad


6

Nie będę powtarzał zwykłych rzeczy na temat używania lub zwalniania niezarządzanych zasobów, które zostały już omówione. Chciałbym jednak zwrócić uwagę na to, co wydaje się powszechnym nieporozumieniem.
Biorąc pod uwagę następujący kod

Klasa publiczna LargeStuff
  Implementuje IDisposable
  Prywatny _Duży jako string ()

  „Dziwny kod, który oznacza, że ​​_Large zawiera teraz kilka milionów długich ciągów.

  Public Sub Dispose () Implementuje IDisposable.Dispose
    _Duże = Nic
  Napis końcowy

Zdaję sobie sprawę, że implementacja jednorazowego użytku nie jest zgodna z aktualnymi wytycznymi, ale mam nadzieję, że wszyscy to rozumiecie.
Teraz, kiedy zostanie wywołane Dispose, ile pamięci zostanie zwolnione?

Odpowiedź: brak.
Wywołanie Dispose może uwolnić niezarządzane zasoby, NIE MOŻE odzyskać pamięci zarządzanej, tylko GC może to zrobić. To nie znaczy, że powyższe nie jest dobrym pomysłem, przestrzeganie powyższego wzoru jest w rzeczywistości dobrym pomysłem. Po uruchomieniu Dispose nic nie stoi na przeszkodzie, aby GC odzyskał pamięć używaną przez _Large, nawet jeśli wystąpienie LargeStuff może być nadal w zasięgu. Ciągi w _Large mogą być również w gen 0, ale wystąpienie LargeStuff może być gen 2, więc ponownie pamięć zostanie ponownie odebrana wcześniej.
Nie ma jednak sensu dodawanie finalizatora do wywołania metody Dispose pokazanej powyżej. Opóźni to ponowne zażądanie pamięci, aby umożliwić działanie finalizatora.


1
Jeśli wystąpienie instancji LargeStuffistnieje wystarczająco długo, aby przejść do Generacji 2, i jeśli _Largezawiera odwołanie do nowo utworzonego ciągu znaków, który znajduje się w Generacji 0, to jeśli wystąpienie LargeStuffjest porzucone bez zerowania _Large, wówczas ciąg, do którego odwołuje się _Largebędą przechowywane do następnej kolekcji Gen2. Zerowanie _Largemoże pozwolić na wyeliminowanie ciągu przy następnej kolekcji Gen0. W większości przypadków wyzerowanie referencji nie jest pomocne, ale są przypadki, w których może przynieść pewne korzyści.
supercat,

5

Oprócz swojej podstawowej stosowania jako sposób kontrolowania życia z zasobów systemowych (całkowicie pokryte przez awesome odpowiedź z Ianem , sława!), Przy czym IDisposable / używając combo mogą być również wykorzystywane do zakresu zmiany stanu (kryzysowych) zasobów globalnych : konsola , że nici The proces , każdy globalny obiekt jak instancji aplikacji .

Napisałem artykuł o tym wzorze: http://pragmateek.com/c-scope-your-global-state-changes-with-idisposable-and-the-using-statement/

Ilustruje, w jaki sposób można chronić często używany stan globalny w sposób wielokrotnego użytku i czytelny : kolory konsoli , bieżąca kultura wątków , właściwości obiektu aplikacji Excel ...


4

Jeśli cokolwiek, spodziewam się, że kod będzie mniej wydajny niż przy jego pomijaniu.

Wywołanie metod Clear () jest niepotrzebne, a GC prawdopodobnie nie zrobiłby tego, gdyby Dispose tego nie zrobił ...


2

W Dispose()przykładowym kodzie są rzeczy, które robi operacja, które mogą wywołać efekt, który nie wystąpiłby z powodu normalnego GC MyCollectionobiektu.

Jeśli do obiektów, do których odwołują się _theListlub _theDictdo których odnoszą się inne obiekty, wówczas przedmiot List<>lub Dictionary<>obiekt nie będzie podlegał kolekcji, ale nagle nie będzie miał żadnej zawartości. Gdyby nie było operacji Dispose () jak w przykładzie, te kolekcje nadal zawierałyby swoją zawartość.

Oczywiście, gdyby tak było, nazwałbym to zepsutym projektem - po prostu wskazuję (pedantycznie, jak sądzę), że Dispose()operacja może nie być całkowicie zbędna, w zależności od tego, czy istnieją inne zastosowania, List<>czy Dictionary<>nie pokazane we fragmencie.


Są to pola prywatne, więc myślę, że to sprawiedliwe, aby założyć, że OP nie podaje do nich odniesień.
mqp

1) fragment kodu jest tylko przykładowym kodem, dlatego wskazuję tylko, że może wystąpić efekt uboczny, który można łatwo przeoczyć; 2) pola prywatne są często celem właściwości / metody gettera - być może zbyt wiele (niektórzy ludzie uważają getter / setters za trochę anty-wzorzec).
Michael Burr

2

Jednym z problemów większości dyskusji na temat „niezarządzanych zasobów” jest to, że tak naprawdę nie definiują tego terminu, ale wydają się sugerować, że ma to coś wspólnego z niezarządzanym kodem. Chociaż prawdą jest, że wiele rodzajów niezarządzanych zasobów współpracuje z niezarządzanym kodem, myślenie o niezarządzanych zasobach w takich kategoriach nie jest pomocne.

Zamiast tego należy rozpoznać, co łączy wszystkie zarządzane zasoby: wszystkie pociągają za sobą obiekt proszący jakąś zewnętrzną „rzecz” o zrobienie czegoś w jej imieniu, ze szkodą dla innych „rzeczy”, a druga jednostka zgadza się to zrobić, dopóki dalsze powiadomienie. Gdyby obiekt został porzucony i zniknął bez śladu, nic nie powiedziałoby temu zewnętrznemu „rzeczowi”, że nie musi już zmieniać swojego zachowania w imieniu obiektu, który już nie istnieje; w konsekwencji użyteczność rzeczy zostałaby trwale zmniejszona.

Zasób niezarządzany reprezentuje zatem zgodę jakiejś zewnętrznej „rzeczy” na zmianę jego zachowania w imieniu obiektu, co bezużyteczne pogorszyłoby użyteczność tej zewnętrznej „rzeczy”, gdyby obiekt został porzucony i przestał istnieć. Zasób zarządzany to obiekt, który jest beneficjentem takiej umowy, ale który podpisał się, aby otrzymywać powiadomienia, jeśli zostanie porzucony, i który wykorzysta takie powiadomienie, aby uporządkować swoje sprawy przed jego zniszczeniem.


Cóż, IMO, definicja niezarządzanego obiektu jest jasna; dowolny obiekt inny niż GC .
Eonil

1
@Eonil: Unmanaged Object! = Niezarządzany zasób. Rzeczy takie jak zdarzenia mogą być realizowane całkowicie przy użyciu zarządzanych obiektów, ale nadal stanowią niezarządzane zasoby, ponieważ - przynajmniej w przypadku obiektów krótkotrwałych subskrybujących zdarzenia obiektów długowiecznych - GC nie wie nic o tym, jak je wyczyścić .
supercat


2

Pierwsza z definicji. Dla mnie niezarządzany zasób oznacza pewną klasę, która implementuje interfejs IDisposable lub coś stworzonego przy użyciu wywołań dll. GC nie wie, jak radzić sobie z takimi obiektami. Jeśli klasa ma na przykład tylko typy wartości, nie uważam tej klasy za klasę z niezarządzanymi zasobami. W moim kodzie stosuję kolejne praktyki:

  1. Jeśli utworzona przeze mnie klasa wykorzystuje pewne niezarządzane zasoby, oznacza to, że powinienem również zaimplementować interfejs IDisposable w celu wyczyszczenia pamięci.
  2. Oczyść przedmioty, jak tylko przestanę z nich korzystać.
  3. W metodzie dispose iteruję wszystkich członków klasy IDisposable i wywołuję Dispose.
  4. W mojej metodzie Dispose wywołaj GC.SuppressFinalize (this), aby powiadomić śmieciarz, że mój obiekt został już wyczyszczony. Robię to, ponieważ wywoływanie GC jest kosztowną operacją.
  5. Jako dodatkowe zabezpieczenie staram się umożliwić wielokrotne wywoływanie Dispose ().
  6. Kiedyś dodaję członka prywatnego _disposed i melduję wywołania metod, czy obiekt został wyczyszczony. A jeśli został wyczyszczony, wygeneruj ObjectDisposedException
    Poniższy szablon demonstruje to, co opisałem słowami jako próbkę kodu:

public class SomeClass : IDisposable
    {
        /// <summary>
        /// As usually I don't care was object disposed or not
        /// </summary>
        public void SomeMethod()
        {
            if (_disposed)
                throw new ObjectDisposedException("SomeClass instance been disposed");
        }

        public void Dispose()
        {
            Dispose(true);
        }

        private bool _disposed;

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;
            if (disposing)//we are in the first call
            {
            }
            _disposed = true;
        }
    }

1
„Dla mnie niezarządzany zasób oznacza pewną klasę, która implementuje interfejs IDisposable lub coś stworzonego przy użyciu wywołań dll”. Mówisz więc, że każdy typ, który is IDisposablesam powinien być uważany za niezarządzany zasób? To nie wydaje się poprawne. Również jeśli typ implementacji jest typem czystej wartości, wydaje się sugerować, że nie trzeba go usuwać. To też wydaje się złe.
Aluan Haddad

Wszyscy sami oceniają. Nie lubię dodawać do mojego kodu czegoś tylko ze względu na dodanie. Oznacza to, że jeśli dodam IDisposable, oznacza to, że stworzyłem jakąś funkcjonalność, której GC nie może zarządzać lub przypuszczam, że nie będzie w stanie właściwie zarządzać swoim czasem życia.
Jurij Zaletskij

2

Podany przykładowy kod nie jest dobrym przykładem IDisposableużycia. Czyszczenie słownika normalnie nie powinno iść do Disposemetody. Elementy słownika zostaną usunięte i usunięte, gdy wykroczą poza zakres. IDisposableimplementacja jest wymagana, aby zwolnić niektóre pamięci / moduły obsługi, które nie zostaną zwolnione / zwolnione nawet po wyjściu poza zakres.

Poniższy przykład pokazuje dobry przykład wzoru IDisposable z pewnym kodem i komentarzami.

public class DisposeExample
{
    // A base class that implements IDisposable. 
    // By implementing IDisposable, you are announcing that 
    // instances of this type allocate scarce resources. 
    public class MyResource: IDisposable
    {
        // Pointer to an external unmanaged resource. 
        private IntPtr handle;
        // Other managed resource this class uses. 
        private Component component = new Component();
        // Track whether Dispose has been called. 
        private bool disposed = false;

        // The class constructor. 
        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // Implement IDisposable. 
        // Do not make this method virtual. 
        // A derived class should not be able to override this method. 
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method. 
            // Therefore, you should call GC.SupressFinalize to 
            // take this object off the finalization queue 
            // and prevent finalization code for this object 
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios. 
        // If disposing equals true, the method has been called directly 
        // or indirectly by a user's code. Managed and unmanaged resources 
        // can be disposed. 
        // If disposing equals false, the method has been called by the 
        // runtime from inside the finalizer and you should not reference 
        // other objects. Only unmanaged resources can be disposed. 
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called. 
            if(!this.disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources. 
                if(disposing)
                {
                    // Dispose managed resources.
                    component.Dispose();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here. 
                // If disposing is false, 
                // only the following code is executed.
                CloseHandle(handle);
                handle = IntPtr.Zero;

                // Note disposing has been done.
                disposed = true;

            }
        }

        // Use interop to call the method necessary 
        // to clean up the unmanaged resource.
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // Use C# destructor syntax for finalization code. 
        // This destructor will run only if the Dispose method 
        // does not get called. 
        // It gives your base class the opportunity to finalize. 
        // Do not provide destructors in types derived from this class.
        ~MyResource()
        {
            // Do not re-create Dispose clean-up code here. 
            // Calling Dispose(false) is optimal in terms of 
            // readability and maintainability.
            Dispose(false);
        }
    }
    public static void Main()
    {
        // Insert code here to create 
        // and use the MyResource object.
    }
}

1

Najbardziej uzasadnionym przypadkiem wykorzystania do rozporządzania zarządzanymi zasobami jest przygotowanie GC do odzyskania zasobów, które w przeciwnym razie nigdy nie zostałyby zebrane.

Najlepszym przykładem są odwołania cykliczne.

Chociaż najlepszą praktyką jest stosowanie wzorców, które unikają odwołań cyklicznych, jeśli w końcu otrzymamy (na przykład) obiekt „podrzędny”, który ma odwołanie z powrotem do swojego „obiektu nadrzędnego”, może to zatrzymać gromadzenie GC elementu nadrzędnego, jeśli po prostu porzucisz referencja i polegaj na GC - a jeśli już zaimplementowałeś finalizator, nigdy nie zostanie on wywołany.

Jedynym sposobem na obejście tego problemu jest ręczne przerwanie odwołań cyklicznych poprzez ustawienie odniesienia potomnego na wartości zerowe dla dzieci.

Wdrażanie IDisposable u rodziców i dzieci jest najlepszym sposobem na to. Gdy wywołanie Dispose zostanie wywołane na obiekcie Parent, wywołanie Dispose na wszystkich elementach podrzędnych, aw metodzie potomnej Dispose ustaw wartość referencji elementu nadrzędnego na null.


4
W większości przypadków GC nie działa, identyfikując martwe obiekty, ale raczej identyfikując żywe obiekty. Po każdym cyklu GC dla każdego obiektu, który zarejestrował się do finalizacji, jest przechowywany na stercie dużego obiektu lub jest celem obiektu na żywo WeakReference, system sprawdzi flagę wskazującą, że w ostatnim cyklu GC znaleziono referencję zrootowaną na żywo i doda obiekt do kolejki obiektów wymagających natychmiastowej finalizacji, zwolni obiekt ze sterty dużych obiektów lub unieważni słabe odniesienie. Refleksje cykliczne nie utrzymują obiektów przy życiu, jeśli nie istnieją żadne inne referencje.
supercat

1

Widzę, że wiele odpowiedzi przesunęło się, aby mówić o używaniu IDisposable zarówno dla zasobów zarządzanych, jak i niezarządzanych. Sugeruję ten artykuł jako jedno z najlepszych wyjaśnień, jakie znalazłem dla tego, jak właściwie należy używać IDisposable.

https://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

Dla rzeczywistego pytania; jeśli używasz IDisposable do czyszczenia zarządzanych obiektów, które zajmują dużo pamięci, krótka odpowiedź brzmiałaby „ nie” . Powodem jest to, że po usunięciu przedmiotu IDisposable powinieneś pozwolić mu wyjść poza zakres. W tym momencie wszelkie obiekty potomne, do których istnieją odniesienia, również są poza zakresem i zostaną zebrane.

Jedynym prawdziwym wyjątkiem jest sytuacja, gdy masz dużo pamięci związanej z obiektami zarządzanymi i blokujesz ten wątek, czekając na zakończenie jakiejś operacji. Jeśli te obiekty, które nie będą potrzebne po zakończeniu tego wywołania, ustawienie tych odwołań na wartość NULL może umożliwić modułowi odśmiecania gromadzenie ich wcześniej. Ale ten scenariusz reprezentowałby zły kod, który musiał zostać zrefaktoryzowany - nie przypadek użycia IDisposable.


1
Nie zrozumiałem, dlaczego ktoś umieścił -1 na twoją odpowiedź
Sebastian Oscar Lopez
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.