Kto powinien wywoływać Disposable na IDisposable objects po przekazaniu do innego obiektu?

Czy istnieją jakieś wskazówki lub najlepsze praktyki, które powinny wywoływać Dispose() na obiektach jednorazowego użytku, gdy zostały one przekazane do metod lub konstuktora innego obiektu?

Oto kilka przykładów, co mam na myśli.

IDisposable obiekt jest przekazywany do metody (czy powinien się go pozbyć po jej wykonaniu?):

public void DoStuff(IDisposable disposableObj)
{
    // Do something with disposableObj
    CalculateSomething(disposableObj)

    disposableObj.Dispose();
}

Obiekt IDisposable jest przekazywany do metody i przechowywany jest Referencja (czy powinien się go pozbyć, gdy MyClass jest usuwany?):

public class MyClass : IDisposable
{
    private IDisposable _disposableObj = null;

    public void DoStuff(IDisposable disposableObj)
    {
        _disposableObj = disposableObj;
    }

    public void Dispose()
    {
        _disposableObj.Dispose();
    }
}

Jestem obecnie myśląc, że w pierwszym przykładzie wywołujący z DoStuff() powinien pozbyć się obiektu, ponieważ prawdopodobnie go stworzył. Ale w drugim przykładzie wydaje się, że MyClass powinien pozbyć się obiektu, ponieważ zachowuje do niego odniesienie. Problem polega na tym, że Klasa wywołująca może nie wiedzieć, że MyClass zachowała odniesienie i dlatego może zdecydować się na pozbycie się obiektu zanim MyClass skończy go używać. Czy są jakieś standardowe zasady dla tego typu scenariuszy? Jeśli są, to czy różnią się, kiedy obiekt jednorazowego użytku jest przekazywany do konstruktora?

Author: James Bateson, 2010-11-03

5 answers

Ogólna zasada jest taka, że jeśli stworzyłeś (lub przejąłeś własność) przedmiot, to twoim obowiązkiem jest go zbyć. Oznacza to, że jeśli otrzymujesz obiekt jednorazowy jako parametr w metodzie lub konstruktorze, zazwyczaj nie powinieneś go wyrzucać.

Zauważ, że niektóre klasy w. NET Framework usuwają obiekty, które otrzymały jako parametry. Na przykład pozbycie się StreamReader usuwa również bazowe Stream.

 35
Author: Mark Byers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-03 10:21:48

P. s.: opublikowałem nową odpowiedź (zawierającą prosty zestaw reguł, które powinny wywoływać Dispose i jak zaprojektować API, które zajmuje się obiektami IDisposable). Podczas gdy obecna odpowiedź zawiera cenne pomysły, doszedłem do przekonania, że jej główna sugestia często nie zadziała w praktyce: ukrywanie IDisposable przedmiotów w "grubszych" przedmiotach często oznacza, że muszą one stać się {1]} sobą; więc kończy się tam, gdzie się zaczęło, a problem polega na tym, że nie można ich znaleźć. szczątki.


Czy istnieją jakieś wskazówki lub najlepsze praktyki dotyczące tego, kto powinien wywoływać Dispose() na obiektach jednorazowego użytku, gdy zostały one przekazane do metod lub konstuktora innego obiektu?

Krótka odpowiedź:

Tak, jest wiele rad na ten temat, a najlepsze, co znam, to Eric Evans ' koncepcja agregatów W Domain-Driven Design . (Mówiąc najprościej, podstawową ideą zastosowaną do IDisposable jest to: zamknąć IDisposable w grubszym ziarnistym komponencie tak, że nie jest widziany przez zewnątrz i nigdy nie jest przekazywany konsumentowi komponentu.)

Co więcej, idea, że twórca IDisposable obiektu powinien być również odpowiedzialny za pozbycie się go, jest zbyt restrykcyjna i często nie sprawdza się w praktyce.

Reszta mojej odpowiedzi jest bardziej szczegółowa w obu punktach, w tej samej kolejności. Zakończę swoją odpowiedź kilkoma wskazówkami do dalszych materiałów, które są związane z tym samym temat.

Dłuższa odpowiedź - o co chodzi w tym pytaniu w szerszym ujęciu:

Porady na ten temat zazwyczaj nie są specyficzne dla IDisposable. Ilekroć ludzie mówią o życiu i własności przedmiotów, odnoszą się do tego samego zagadnienia (ale bardziej ogólnie).

Dlaczego ten temat prawie nigdy nie pojawia się w ekosystemie. NET? Ponieważ środowisko uruchomieniowe. NET (CLR) wykonuje automatyczne usuwanie śmieci, które wykonuje całą pracę za Ciebie: jeśli nie dłużej potrzebny jest obiekt, można po prostu o nim zapomnieć, a garbage collector w końcu odzyska jego pamięć.

Dlaczego w takim razie pojawia się pytanie o IDisposable obiekty? Ponieważ IDisposable polega na wyraźnej, deterministycznej kontroli czasu życia (często rzadkiego lub kosztownego) zasobu: IDisposable obiekty powinny zostać uwolnione, gdy tylko nie będą już potrzebne - a nieokreślona gwarancja garbage collector ("I' ll W końcu odzyskam zasoby. pamięć używana przez Ciebie!") po prostu nie jest wystarczająco dobry.

Twoje pytanie, ponownie sformułowane w szerszych kategoriach życia obiektu i własności:

Który obiekt O powinien być odpowiedzialny za zakończenie życia (jednorazowego użytku) obiektu D, która również jest przekazywana do obiektów X,Y,Z?

Ustalmy kilka założeń:

  • Wywołanie D.Dispose() dla IDisposable obiektu D zasadniczo kończy swój okres życia.

  • Logicznie rzecz biorąc, żywotność obiektu może być zakończona tylko raz. (Nie ważne na razie, że stoi to w opozycji do protokołu IDisposable, który wyraźnie zezwala na wielokrotne wywołania do Dispose.)

  • Dlatego, ze względu na prostotę, dokładnie jeden przedmiot O powinien być odpowiedzialny za zbywanie D. Zadzwońmy O właściciel.

Teraz my do sedna problemu: ani język C#, ani VB.NET zapewnienie mechanizmu egzekwowania stosunków własności między obiektami. Więc to zamienia się w problem projektowy: wszystkie obiekty O,X,Y,Z które otrzymują odniesienie do innego obiektu D musi przestrzegać i stosować się do konwencji, która dokładnie reguluje, kto ma prawo do D.

Uprość problem z agregatami!

Jedna najlepsza rada, jaką znalazłem w tej temat pochodzi z Eric Evans ' 2004 książka, Domain-Driven Design . Pozwolę sobie zacytować z książki:

powiedzmy, że usuwałeś obiekt Person z bazy danych. Wraz z osobą go imię, data urodzenia, i opis pracy. Ale co z adresem? Mogą być inne osoby pod tym samym adresem. Jeśli usuniesz adres, obiekty osoby będą miały odniesienia do usuniętego obiektu. Jeśli go zostawisz, gromadzisz adresy śmieci w bazie danych. Automatyczne usuwanie śmieci może wyeliminować niepotrzebne adresy, ale ta poprawka techncal, nawet jeśli jest dostępna w systemie bazodanowym, ignoruje podstawowy problem z modelowaniem. (str. 125)

Widzisz, jaki to ma związek z Twoim problemem? Adresy z tego przykładu są równoważne z przedmiotami jednorazowego użytku, a pytania są takie same: kto powinien je usunąć? Kto je "posiada"?

Evans dalej proponuje Agregaty jako rozwiązanie tego problemu projektowego. Z książki jeszcze raz:

agregat to klaster powiązanych obiektów, które traktujemy jako jednostkę w celu zmiany danych. Każdy agregat ma korzeń i granicę. Granica określa, co znajduje się wewnątrz agregatu. Korzeń jest pojedynczym, specyficznym podmiotem zawartym w agregacie. Root jest jedynym członkiem agregatu, do którego obiekty zewnętrzne mogą zawierać odniesienia, chociaż obiekty w granicach mogą zawierać odniesienia do siebie nawzajem. (pp. 126-127)

Główną wiadomością jest to, że powinieneś ograniczyć przekazywanie swojego IDisposable obiektu do ściśle ograniczonego zbioru ("agregatu") innych obiektów. Obiekty spoza tej zagregowanej granicy nigdy nie powinny mieć bezpośredniego odniesienia do twojej IDisposable. To znacznie upraszcza rzeczy, ponieważ nie musisz się już martwić, czy największa część wszystkich obiektów, a mianowicie te spoza agregatu, może Dispose Twój obiekt. Wszystko, co musisz zrobić, to upewnić się, że obiekty wewnątrz granica wszyscy wiedzą, kto jest odpowiedzialny za pozbycie się jej. Powinno to być wystarczająco łatwe do rozwiązania, ponieważ zwykle wdrażasz je razem i dbasz o to, aby granice agregatu były rozsądnie "ciasne".

A co z sugestią, że twórca IDisposable obiektu również powinien się go pozbyć?

Ta wskazówka brzmi rozsądnie i jest w niej atrakcyjna symetria, ale sama w sobie często nie zadziała w praktyce. Prawdopodobnie to oznacza to to samo co powiedzenie: "Nigdy nie przekazuj odniesienia do IDisposable obiektu innemu obiektowi", ponieważ gdy tylko to zrobisz, ryzykujesz, że obiekt otrzymujący przejmie jego własność i pozbędzie się go bez Twojej wiedzy.

Przyjrzyjmy się dwóm znanym typom interfejsów z biblioteki. NET Base Class Library (BCL), które wyraźnie naruszają tę regułę: IEnumerable<T> i IObservable<T>. Obie są zasadniczo fabrykami, które zwracają IDisposable obiekty:

  • IEnumerator<T> IEnumerable<T>.GetEnumerator()
    (Pamiętaj że IEnumerator<T> dziedziczy z IDisposable.)

  • IDisposable IObservable<T>.Subscribe(IObserver<T> observer)

W obu przypadkach oczekuje się, że wywołujący usunie zwracany obiekt. Prawdopodobnie nasze wytyczne po prostu nie mają sensu w przypadku fabryk obiektów... chyba, że wymagamy, aby requester (Nie jego bezpośredni twórca ) IDisposable go uwolnił.

Nawiasem mówiąc, ten przykład pokazuje również granice rozwiązania agregatu opisane powyżej: Zarówno IEnumerable<T> jak i IObservable<T> są zbyt ogólne w naturze, aby kiedykolwiek być częścią agregatu. Agregaty są zazwyczaj bardzo specyficzne dla danej dziedziny.

Dalsze zasoby i pomysły:

 36
Author: stakx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:35

Ogólnie rzecz biorąc, gdy masz do czynienia z obiektem jednorazowym, nie jesteś już w idealnym świecie zarządzanego kodu, gdzie dożywotnia własność jest kwestią sporną. W rezultacie musisz wziąć pod uwagę, jaki obiekt logicznie "posiada"lub jest odpowiedzialny za żywotność twojego jednorazowego obiektu.

Ogólnie Rzecz Biorąc, w przypadku obiektu jednorazowego użytku, który jest właśnie przekazany do metody, powiedziałbym Nie, metoda nie powinna pozbywać się obiektu, ponieważ bardzo rzadko jeden obiekt przejmuje własność innego obiektu, a następnie zrobić z nim w tej samej metodzie. Rozmówca powinien być odpowiedzialny za usuwanie w tych przypadkach.

Nie ma automatycznej odpowiedzi, która mówi "tak, zawsze usuwaj" lub "nie, nigdy nie usuwaj", gdy mówimy o danych członkowskich. Raczej musisz myśleć o obiektach w każdym konkretnym przypadku i zadać sobie pytanie: "czy ten obiekt jest odpowiedzialny za żywotność obiektu jednorazowego użytku?"

Zasadą jest, że obiekt odpowiedzialny za tworzenie jednorazowego jest właścicielem, a zatem jest odpowiedzialny za pozbywanie się go później. To się nie uda, jeśli dojdzie do przeniesienia własności. Na przykład:

public class Foo
{
    public MyClass BuildClass()
    {
        var dispObj = new DisposableObj();
        var retVal = new MyClass(dispObj);
        return retVal;
    }
}

Foo jest wyraźnie odpowiedzialny za tworzenie dispObj, ale przekazuje własność instancji MyClass.

 8
Author: Greg D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-03 10:19:22

Jest to kontynuacja mojej poprzedniej odpowiedzi ; zobacz jej początkową uwagę, aby dowiedzieć się, dlaczego zamieszczam inną.

Moja poprzednia odpowiedź ma jedną rację: każdy IDisposable powinien mieć wyłącznego "właściciela", który będzie odpowiedzialny za Dispose-ing z niego dokładnie raz. zarządzanie obiektami IDisposable staje się bardzo porównywalne do zarządzania pamięcią w scenariuszach kodu niezarządzanego.

Poprzednia technologia. NET, Component Object Model (COM), używała następujących protokół do zarządzania pamięcią obowiązki pomiędzy obiektami:

  • " parametry In muszą być przydzielone i zwolnione przez wywołującego.
  • " Out-parametry muszą być przydzielane przez wywołanego; są one uwalniane przez wywołującego [...].
  • " in-out-parametry są początkowo przydzielane przez wywołującego, a następnie zwalniane i realokowane przez wywołującego, jeśli to konieczne. Podobnie jak w przypadku parametrów wyjściowych, wywołujący jest odpowiedzialny za uwolnienie końcowego zwracanego wartość."

(istnieją dodatkowe zasady dotyczące przypadków błędów; szczegóły można znaleźć na stronie podlinkowanej powyżej.)

Jeśli mielibyśmy dostosować te wytyczne do IDisposable, moglibyśmy określić następujące...]}

Zasady dotyczące IDisposable własności:

  1. gdy IDisposable jest przekazywana do metody poprzez zwykły parametr, nie ma przeniesienia własności. Wywołana metoda może używać IDisposable, ale nie może Dispose it (ani przekazać własności; patrz reguła 4 poniżej).
  2. gdy IDisposable jest zwracana z metody za pomocą parametru out lub wartości zwracanej, wtedy własność jest przenoszona z metody do jej wywołującego. Wywołujący będzie musiał Dispose go (lub przekazać własność IDisposable w ten sam sposób).
  3. gdy IDisposable jest dana metodzie za pomocą parametru ref, wtedy własność nad nią jest przenoszona do tej metody. Metoda powinna skopiować IDisposable do lokalnego pola zmiennej lub obiektu, a następnie ustawić parametr ref na null.
Z powyższego wynika jedna prawdopodobnie ważna zasada:]}
  1. jeśli nie masz prawa własności, nie możesz jej przekazać dalej. Oznacza to, że jeśli otrzymałeś obiekt IDisposable za pomocą zwykłego parametru, nie umieszczaj tego samego obiektu w parametrze ref IDisposable, ani nie wystawiaj go za pomocą wartości zwracanej lub parametru out.

Przykład:

sealed class LineReader : IDisposable
{
    public static LineReader Create(Stream stream)
    {
        return new LineReader(stream, ownsStream: false);
    }

    public static LineReader Create<TStream>(ref TStream stream) where TStream : Stream
    {
        try     { return new LineReader(stream, ownsStream: true); }
        finally { stream = null;                                   }
    }

    private LineReader(Stream stream, bool ownsStream)
    {
        this.stream = stream;
        this.ownsStream = ownsStream;
    }

    private Stream stream; // note: must not be exposed via property, because of rule (2)
    private bool ownsStream;

    public void Dispose()
    {
        if (ownsStream)
        {
            stream?.Dispose();
        }
    }

    public bool TryReadLine(out string line)
    {
        throw new NotImplementedException(); // read one text line from `stream` 
    }
}

Ta klasa ma dwie statyczne metody fabryczne i tym samym pozwala klientowi wybrać, czy chce zachować, czy przekazać dalej własność:

  • Przyjmuje się obiekt Stream za pomocą zwykłego parametru. Sygnalizuje to dzwoniącemu, że własność nie zostanie przejęta. Tak więc dzwoniący musi Dispose:

    using (var stream = File.OpenRead("Foo.txt"))
    using (var reader = LineReader.Create(stream))
    {
        string line;
        while (reader.TryReadLine(out line))
        {
            Console.WriteLine(line);
        }
    }
    
  • Taki, który akceptuje obiekt Stream poprzez parametr ref. Jest to sygnał do wywołującego, że własność zostanie przeniesiona, więc wywołujący nie musi Dispose:

    var stream = File.OpenRead("Foo.txt");
    using (var reader = LineReader.Create(ref stream))
    {
        string line;
        while (reader.TryReadLine(out line))
        {
            Console.WriteLine(line);
        }
    }
    

    Co ciekawe, jeśli stream zostaną zadeklarowane jako using zmienna: using (var stream = …), kompilacja nie powiedzie się ponieważ zmienne using nie mogą być przekazywane jako parametry ref, więc kompilator C# pomaga egzekwować nasze reguły w tym konkretnym przypadku.

Na koniec zauważ, że File.OpenRead jest przykładem metody, która zwraca obiekt IDisposable (mianowicie obiekt Stream) poprzez zwracaną wartość, więc własność nad zwracanym strumieniem jest przekazywana do wywołującego.

Główną wadą tego wzoru jest to, że AFAIK, nikt go nie używa (jeszcze). Więc jeśli korzystasz z dowolnego API, które nie przestrzega powyższych reguł (na przykład biblioteki klas bazowych. NET Framework) musisz jeszcze przeczytać dokumentację, aby dowiedzieć się, kto ma wywoływać Dispose na obiektach IDisposable.
 7
Author: stakx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:15

Jedną z rzeczy, którą postanowiłem zrobić, zanim dowiedziałem się wiele o programowaniu. NET, ale nadal wydaje się dobrym pomysłem, jest posiadanie konstruktora, który akceptuje IDisposable również akceptuje Boolean, który mówi, czy własność obiektu zostanie przeniesiona, jak również. W przypadku obiektów, które mogą istnieć całkowicie w zakresie instrukcji using, nie będzie to na ogół zbyt ważne (ponieważ zewnętrzny obiekt zostanie usunięty w zakresie używanego bloku wewnętrznego obiektu, nie ma potrzeby stosowania zewnętrznego obiektu aby pozbyć się wewnętrznego; w rzeczywistości może być konieczne, aby tego nie robił). Taka semantyka może jednak stać się niezbędna, gdy zewnętrzny obiekt zostanie przekazany jako interfejs lub klasa bazowa do kodu, który nie wie o istnieniu wewnętrznego obiektu. W takim przypadku wewnętrzny obiekt powinien żyć, dopóki zewnętrzny obiekt nie zostanie zniszczony, a rzeczą, która wie, że wewnętrzny obiekt powinien umrzeć, gdy zewnętrzny obiekt to robi, jest zewnętrzny obiekt sam w sobie, więc zewnętrzny obiekt musi być w stanie zniszczyć zewnętrzny obiekt. wewnętrzny.

Od tego czasu, miałem kilka dodatkowych pomysłów, ale nie próbowałem ich. Ciekawi mnie co myślą inni:

  1. wrapper z liczeniem referencji dla IDisposable obiektu. Tak naprawdę nie odkryłem najbardziej naturalnego wzorca do tego, ale jeśli obiekt używa zliczania referencji z blokowanym inkrementem/dekrementem, i jeśli (1) cały kod, który manipuluje obiektem, używa go poprawnie, i (2) nie są tworzone cykliczne odwołania za pomocą obiektu, spodziewam się, że że powinno być możliwe posiadanie współdzielonego IDisposable obiektu, który zostanie zniszczony, gdy ostatnie użycie przejdzie na pa-pa. Prawdopodobnie powinno się zdarzyć, że Klasa publiczna powinna być opakowaniem dla prywatnej klasy z liczeniem referencji i powinna wspierać konstruktor lub metodę fabryczną, która utworzy nowy opakowaniem dla tej samej instancji bazowej(zmniejszając liczbę referencji instancji o jeden). Lub, jeśli klasa musi być oczyszczona nawet wtedy, gdy owijki są opuszczone, i jeśli klasa ma jakieś klasa może przechowywać listę WeakReferences w swoich opakowaniach i sprawdzać, czy przynajmniej niektóre z nich nadal istnieją.
  2. niech konstruktor obiektu IDisposable zaakceptuje delegata, który wywoła przy pierwszym usunięciu obiektu (obiekt IDisposable powinien użyć Interlocked.Exchange na fladze isDisposed, aby upewnić się, że zostanie usunięty dokładnie raz). Delegat ten może następnie zająć się usuwaniem zagnieżdżonych obiektów (ewentualnie sprawdzeniem, czy ktoś jeszcze trzyma ich).

Czy któryś z nich wydaje się dobrym wzorem?

 2
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-03 17:22:11