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?
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
.
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ę obiektamiIDisposable
). Podczas gdy obecna odpowiedź zawiera cenne pomysły, doszedłem do przekonania, że jej główna sugestia często nie zadziała w praktyce: ukrywanieIDisposable
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) obiektuD
, która również jest przekazywana do obiektówX,Y,Z
?
Ustalmy kilka założeń:
Wywołanie
D.Dispose()
dlaIDisposable
obiektuD
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 doDispose
.)Dlatego, ze względu na prostotę, dokładnie jeden przedmiot
O
powinien być odpowiedzialny za zbywanieD
. ZadzwońmyO
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:
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"?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)
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 żeIEnumerator<T>
dziedziczy zIDisposable
.)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:
W UML relacje "ma" między obiektami mogą być modelowane na dwa sposoby: jako agregacja (pusty diament) lub jako kompozycja (wypełniony diament). Skład różni się od agregacji tym, że okres życia obiektu zawierającego/odsyłanego kończy się okresem życia kontenera/odsyłającego. Twój oryginalny pytanie zakłada agregację ("zbywalna własność"), podczas gdy ja głównie kieruję się w kierunku rozwiązań, które używają kompozycji ("stała własność"). Zobacz artykuł w Wikipedii na temat "skład obiektu" .
Autofac (A. NET IoC container) rozwiązuje ten problem na dwa sposoby: albo komunikując się, używając tak zwanego typu relacji ,
Owned<T>
, który nabywa własność nadIDisposable
; lub poprzez pojęcie jednostek praca, tzw. dożywotnie zakresy w Autofac.W związku z tym, Nicholas Blumhardt, twórca Autofac, napisał "Autofac Lifetime Primer"[49], który zawiera sekcję "IDisposable and ownership". Cały artykuł jest znakomitym traktatem na temat własności i kwestii życiowych w .NET. polecam lekturę, nawet tym, którzy nie interesują się Autofac.
W C++ nabywanie zasobów to idiom Initialization (RAII) (W inteligentne typy wskaźników (w szczególności) pomagają programistom uzyskać prawo do życia i własności obiektów. Niestety, nie można ich przenieść do. NET, ponieważ. NET nie ma eleganckiego wsparcia C++dla deterministycznego niszczenia obiektów.
Zobacz również tę odpowiedź na pytanie o przepełnienie stosu, "jak rozliczać różne potrzeby implementacji?", która (jeśli dobrze rozumiem) podąża za podobną myślą jak moja Odpowiedź oparta na agregacie: Budowa gruboziarnistego składnika wokół
IDisposable
tak, że jest on całkowicie zawarty (i ukryty przed konsumentem składnika) wewnątrz.
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
.
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 doIDisposable
, moglibyśmy określić następujące...]}
Zasady dotyczące IDisposable
własności:
- 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żeDispose
it (ani przekazać własności; patrz reguła 4 poniżej). - gdy
IDisposable
jest zwracana z metody za pomocą parametruout
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). - gdy
IDisposable
jest dana metodzie za pomocą parametruref
, wtedy własność nad nią jest przenoszona do tej metody. Metoda powinna skopiowaćIDisposable
do lokalnego pola zmiennej lub obiektu, a następnie ustawić parametrref
nanull
.
- 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 parametrzeref IDisposable
, ani nie wystawiaj go za pomocą wartości zwracanej lub parametruout
.
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 musiDispose
: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 parametrref
. Jest to sygnał do wywołującego, że własność zostanie przeniesiona, więc wywołujący nie musiDispose
: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 jakousing
zmienna:using (var stream = …)
, kompilacja nie powiedzie się ponieważ zmienneusing
nie mogą być przekazywane jako parametryref
, 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
.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:
- 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ółdzielonegoIDisposable
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ęWeakReference
s w swoich opakowaniach i sprawdzać, czy przynajmniej niektóre z nich nadal istnieją. - niech konstruktor obiektu
IDisposable
zaakceptuje delegata, który wywoła przy pierwszym usunięciu obiektu (obiektIDisposable
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?
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