Czy wyciek pamięci jest tworzony, jeśli MemoryStream w .NET nie jest zamknięty?


113

Mam następujący kod:

MemoryStream foo(){
    MemoryStream ms = new MemoryStream();
    // write stuff to ms
    return ms;
}

void bar(){
    MemoryStream ms2 = foo();
    // do stuff with ms2
    return;
}

Czy jest jakaś szansa, że ​​przydzielony przeze mnie MemoryStream jakoś nie zostanie później usunięty?

Otrzymałem wzajemną recenzję, w której nalegałem, abym ręcznie to zamknął i nie mogę znaleźć informacji, aby stwierdzić, czy ma on ważny punkt, czy nie.


41
Zapytaj swojego recenzenta, dlaczego uważa, że ​​powinieneś go zamknąć. Jeśli mówi o ogólnej dobrej praktyce, prawdopodobnie jest sprytny. Jeśli mówi o wcześniejszym uwalnianiu pamięci, to się myli.
Jon Skeet,

Odpowiedzi:


60

Jeśli coś jest jednorazowego użytku, zawsze należy to wyrzucić. Powinieneś używać usinginstrukcji w swojej bar()metodzie, aby upewnić się, że ms2zostanie usunięty.

W końcu zostanie wyczyszczony przez zbieracz śmieci, ale zawsze dobrze jest wywoływać funkcję Dispose. Jeśli uruchomisz FxCop na swoim kodzie, oznaczałoby to jako ostrzeżenie.


16
Korzystanie z połączeń blokowych jest usuwane za Ciebie.
Nick,

20
@Grauenwolf: Twoje stwierdzenie przerywa hermetyzację. Jako konsument nie powinieneś przejmować się tym, czy nie jest to operacja: jeśli jest to IDisposable, Twoim zadaniem jest Dispose () to.
Marc Gravell

4
Nie jest to prawdą w przypadku klasy StreamWriter: usunie połączony strumień tylko wtedy, gdy usuniesz StreamWriter - nigdy nie usunie strumienia, jeśli zbierze elementy bezużyteczne, a jego finalizator zostanie wywołany - jest to zgodne z projektem.
springy76

4
Wiem, że to pytanie było z 2008 roku, ale dzisiaj mamy bibliotekę zadań .NET 4.0. Dispose () jest niepotrzebne w większości przypadków podczas korzystania z zadań. Chociaż zgodziłbym się, że IDisposable powinno oznaczać „Lepiej pozbądź się tego, kiedy skończysz”, to już tak naprawdę nie oznacza.
Phil

7
Innym przykładem, w którym nie należy usuwać obiektu IDisposable, jest HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong Po prostu inny przykład z BCL, w którym istnieje obiekt IDisposable i nie trzeba (a nawet nie należy) go usuwać to. To tylko dla przypomnienia, że ​​zazwyczaj są wyjątki od ogólnej reguły, nawet w BCL;)
Mariusz Pawelski

167

Nic nie wyciekniesz - przynajmniej w obecnej implementacji.

Wywołanie Dispose nie czyści pamięci używanej przez MemoryStream szybciej. Spowoduje to, że Twój strumień nie będzie zdolny do wykonywania połączeń odczytu / zapisu po połączeniu, co może być dla Ciebie przydatne lub nie.

Jeśli jesteś absolutnie pewien, że nigdy nie chcesz przechodzić z MemoryStream do innego rodzaju strumienia, nie wyrządzisz ci żadnej szkody, jeśli nie wywołasz metody Dispose. Jednakże, jest to generalnie dobra praktyka częściowo dlatego, jeśli kiedykolwiek zrobić zmianę do używania innego Stream, nie chcesz, aby ugryziony przez błąd trudny do zdobycia, ponieważ wybrał łatwe wyjście na początku. (Z drugiej strony istnieje argument YAGNI ...)

Innym powodem, dla którego warto to zrobić, jest fakt, że nowa implementacja może wprowadzić zasoby, które zostaną uwolnione w trybie Dispose.


W tym przypadku funkcja zwraca MemoryStream, ponieważ dostarcza „dane, które mogą być różnie interpretowane w zależności od wywoływanych parametrów”, więc mogłaby to być tablica bajtów, ale z innych powodów była łatwiejsza do wykonania jako MemoryStream. Więc na pewno nie będzie to kolejna klasa Stream.
Coderer

W takim przypadku nadal starałbym się go pozbyć na zasadzie ogólnej - budować dobre nawyki itp. - ale nie martwiłbym się zbytnio, gdyby stało się to trudne.
Jon Skeet

1
Jeśli naprawdę martwisz się o jak najszybsze zwolnienie zasobów, usuń odwołanie natychmiast po bloku „using”, aby niezarządzane zasoby (jeśli takie istnieją) zostały wyczyszczone, a obiekt kwalifikował się do czyszczenia pamięci. Jeśli metoda powraca od razu, prawdopodobnie nie zrobi to dużej różnicy, ale jeśli zaczniesz robić inne rzeczy w metodzie, takie jak żądanie większej ilości pamięci, z pewnością może to mieć znaczenie.
Triynko

@Triynko Nie do końca prawda: Zobacz: stackoverflow.com/questions/574019/… po szczegóły.
George Stocker,

10
Argument YAGNI można rozpatrywać w obie strony - ponieważ decyzja, aby nie wyrzucać czegoś, co implementuje, IDisposablejest szczególnym przypadkiem sprzecznym z normalną najlepszą praktyką, możesz argumentować, że tego przypadku nie powinieneś robić, dopóki naprawdę nie musisz, zgodnie z YAGNI zasada.
Jon Hanna,

26

Tak tam przeciek , w zależności od sposobu zdefiniowania przeciekać i jak dużo później masz na myśli ...

Jeśli przez przeciek masz na myśli "pamięć pozostaje przydzielona, ​​niedostępna do użycia, nawet jeśli skończyłeś z niej korzystać", a przez to w dowolnym momencie po wywołaniu dispose, to tak, może być przeciek, chociaż nie jest trwały (np. czas działania aplikacji).

Aby zwolnić pamięć zarządzaną używaną przez MemoryStream, należy usunąć odwołanie do niej, anulując odwołanie do niej, aby od razu kwalifikowała się do wyrzucania elementów bezużytecznych. Jeśli tego nie zrobisz, utworzysz tymczasowy wyciek od czasu, gdy skończysz go używać, aż twoje odniesienie wyjdzie poza zakres, ponieważ w międzyczasie pamięć nie będzie dostępna do alokacji.

Zaletą instrukcji using (w porównaniu z prostym wywołaniem metody dispose) jest to, że można ZADEKLAROWAĆ swoje odwołanie w instrukcji using. Po zakończeniu instrukcji using nie tylko jest wywoływana dispose, ale również twoje odniesienie wychodzi poza zakres, skutecznie unieważniając odniesienie i czyniąc obiekt natychmiast kwalifikującym się do czyszczenia pamięci bez konieczności pamiętania o napisaniu kodu „reference = null”.

Chociaż brak odwołania do czegoś od razu nie jest klasycznym „trwałym” wyciekiem pamięci, to z pewnością ma ten sam efekt. Na przykład, jeśli zachowasz odniesienie do MemoryStream (nawet po wywołaniu dispose) i nieco dalej w swojej metodzie spróbujesz przydzielić więcej pamięci ... pamięć używana przez twój wciąż przywoływany strumień pamięci nie będzie dostępna do Ciebie, dopóki nie unieważnisz odwołania lub nie wyjdzie ono poza zakres, nawet jeśli wywołałeś dispose i skończyłeś z nim korzystać.


6
Uwielbiam tę odpowiedź. Czasami ludzie zapominają o podwójnym obowiązku używania: gorliwym odzyskiwaniu zasobów i gorliwym dereferencji.
Kit

1
Rzeczywiście, chociaż słyszałem, że w przeciwieństwie do Javy, kompilator C # wykrywa „ostatnie możliwe użycie”, więc jeśli zmienna ma wyjść poza zakres po jej ostatnim odwołaniu, może kwalifikować się do czyszczenia pamięci zaraz po ostatnim możliwym użyciu. , zanim faktycznie wyjdzie poza zakres. Zobacz stackoverflow.com/questions/680550/explicit-nulling
Triynko

2
Odśmiecacz i jitter nie działają w ten sposób. Zakres to konstrukcja języka, a nie coś, czego środowisko wykonawcze będzie przestrzegać. W rzeczywistości prawdopodobnie wydłużasz czas, w którym odwołanie jest w pamięci, dodając wywołanie .Dispose () po zakończeniu bloku. Zobacz ericlippert.com/2015/05/18/…
Pablo Montilla

8

Dzwonienie .Dispose()(lub pakowanie Using) nie jest wymagane.

Powodem, dla którego dzwonisz, .Dispose()jest jak najszybsze zwolnienie zasobu .

Pomyśl o, powiedzmy, serwerze Stack Overflow, gdzie mamy ograniczony zestaw pamięci i tysiące przychodzących żądań. Nie chcemy czekać na zaplanowane czyszczenie pamięci, chcemy zwolnić tę pamięć jak najszybciej, aby była dostępna dla nowych żądań przychodzących.


24
Wywołanie Dispose na MemoryStream nie spowoduje jednak zwolnienia żadnej pamięci. W rzeczywistości nadal możesz uzyskać dostęp do danych w MemoryStream po wywołaniu Dispose - spróbuj :)
Jon Skeet,

12
-1 Chociaż jest to prawdą dla MemoryStream, jako ogólna rada jest to po prostu błędne. Dispose to zwolnienie niezarządzanych zasobów, takich jak uchwyty plików lub połączenia z bazą danych. Pamięć nie należy do tej kategorii. Prawie zawsze należy czekać na zaplanowane wyrzucanie elementów bezużytecznych, aby zwolnić pamięć.
Joe

1
Jakie są zalety przyjęcia jednego stylu kodowania do przydzielania i usuwania FileStreamobiektów, a innego do MemoryStreamobiektów?
Robert Rossney

3
FileStream obejmuje niezarządzane zasoby, które w rzeczywistości mogą zostać natychmiast zwolnione po wywołaniu metody Dispose. Z drugiej strony MemoryStream przechowuje zarządzaną tablicę bajtów w swojej zmiennej _buffer, która nie jest zwalniana w czasie utylizacji. W rzeczywistości _buffer nie jest nawet zerowany w metodzie Dispose MemoryStream, która jest WSTĘPNYM IMO BŁĘDU, ponieważ zerowanie odwołania może sprawić, że pamięć będzie kwalifikować się do GC w momencie dyspozycji. Zamiast tego utrzymująca się (ale usunięta) referencja MemoryStream nadal zachowuje pamięć. Dlatego po usunięciu go należy również wyzerować, jeśli nadal znajduje się w zakresie.
Triynko,

@Triynko - „Dlatego, gdy już go pozbędziesz, powinieneś go również anulować, jeśli nadal jest w zakresie” - nie zgadzam się. Jeśli zostanie użyty ponownie po wywołaniu Dispose, spowoduje to NullReferenceException. Jeśli nie zostanie ponownie użyty po Dispose, nie ma potrzeby anulowania tego; GC jest wystarczająco inteligentny.
Joe

8

Na to już odpowiedź, ale dodam tylko, że dobra, staroświecka zasada ukrywania informacji oznacza, że ​​w przyszłości możesz chcieć dokonać refaktoryzacji:

MemoryStream foo()
{    
    MemoryStream ms = new MemoryStream();    
    // write stuff to ms    
    return ms;
}

do:

Stream foo()
{    
   ...
}

Podkreśla to, że wywołujący nie powinni przejmować się tym, jaki rodzaj strumienia jest zwracany, i umożliwia zmianę implementacji wewnętrznej (np. Podczas mockowania do testów jednostkowych).

Będziesz musiał mieć kłopoty, jeśli nie użyłeś Dispose w swojej implementacji paska:

void bar()
{    
    using (Stream s = foo())
    {
        // do stuff with s
        return;
    }
}

5

Wszystkie strumienie implementują IDisposable. Owiń strumień pamięci w instrukcję using, a wszystko będzie dobrze i elegancko. Blok using zapewni, że Twój strumień zostanie zamknięty i usunięty bez względu na wszystko.

gdziekolwiek zadzwonisz do Foo, możesz to zrobić za pomocą (MemoryStream ms = foo ()) i myślę, że nadal powinno być dobrze.


1
Jednym z problemów, które napotkałem z tym nawykiem, jest to, że musisz mieć pewność, że strumień nie jest używany nigdzie indziej. Na przykład utworzyłem JpegBitmapDecoder wskazujący na MemoryStream i zwróciliśmy ramki [0] (myśląc, że skopiuje dane do własnego magazynu wewnętrznego), ale stwierdziłem, że mapa bitowa będzie się pojawiać tylko w 20% przypadków - okazało się, że to dlatego Pozbywałem się strumienia pamięci.
devios1

Jeśli twój strumień pamięci musi trwać (tj. Blok using nie ma sensu), powinieneś wywołać Dispose i natychmiast ustawić zmienną na null. Jeśli go wyrzucisz, nie będzie już przeznaczone do użytku, więc powinieneś również od razu ustawić go na null. To, co opisuje chaiguy, brzmi jak problem z zarządzaniem zasobami, ponieważ nie powinieneś wydawać referencji do czegoś, chyba że rzecz, której je przekazujesz, bierze odpowiedzialność za jej usunięcie, a rzecz rozdająca referencję wie, że nie jest już odpowiedzialna robiąc tak.
Triynko,

2

Nie wycieknie pamięć, ale recenzent kodu ma rację, wskazując, że należy zamknąć strumień. Grzecznie to zrobić.

Jedyną sytuacją, w której możesz wyciekać pamięć, jest przypadkowe pozostawienie odniesienia do strumienia i nigdy go nie zamykasz. Nadal nie są naprawdę wyciek pamięci, ale niepotrzebnie rozszerzając ilość czasu, który twierdzi, że go używał.


1
> Wciąż nie tracisz pamięci, ale niepotrzebnie wydłużasz czas, w którym twierdzisz, że jej używasz. Jesteś pewny? Funkcja Dispose nie zwalnia pamięci, a wywołanie jej późno w funkcji może w rzeczywistości wydłużyć czas, w którym nie można jej zebrać.
Jonathan Allen,

2
Tak, Jonathan ma rację. Umieszczenie wywołania Dispose późno w funkcji może w rzeczywistości spowodować, że kompilator pomyśli, że musisz uzyskać dostęp do instancji strumienia (aby ją zamknąć) bardzo późno w funkcji. Może to być gorsze niż w ogóle nie wywoływanie metody dispose (stąd unikanie odwołania funkcji do zmiennej stream), ponieważ kompilator mógłby w przeciwnym razie obliczyć optymalny punkt wydania (inaczej „punkt ostatniego możliwego użycia”) wcześniej w funkcji .
Triynko,

2

Polecam owijania w MemoryStream bar()w usingoświadczeniu głównie dla spójności:

  • W tej chwili MemoryStream nie zwalnia włączonej pamięci .Dispose(), ale możliwe jest, że w pewnym momencie w przyszłości może ona lub Ty (lub ktoś inny w Twojej firmie) zastąpi ją Twoim własnym, niestandardowym MemoryStream, który to robi, itp.
  • Pomaga ustalić wzorzec w projekcie, aby zapewnić usunięcie wszystkich strumieni - linia jest wyraźniej nakreślona, ​​mówiąc „wszystkie strumienie muszą zostać usunięte” zamiast „niektóre strumienie muszą zostać usunięte, ale niektóre nie muszą” ...
  • Jeśli kiedykolwiek zmienisz kod, aby umożliwić zwracanie innych typów strumieni, musisz to zmienić, aby mimo to usunąć.

Inną rzeczą, którą zwykle robię w przypadkach, takich jak foo()tworzenie i zwracanie IDisposable, jest zapewnienie, że jakakolwiek awaria między skonstruowaniem obiektu a obiektem returnzostanie przechwycona przez wyjątek, pozbywa się obiektu i ponownie zgłasza wyjątek:

MemoryStream x = new MemoryStream();
try
{
    // ... other code goes here ...
    return x;
}
catch
{
    // "other code" failed, dispose the stream before throwing out the Exception
    x.Dispose();
    throw;
}

1

Jeśli obiekt implementuje IDisposable, po zakończeniu należy wywołać metodę .Dispose.

W niektórych obiektach Dispose oznacza to samo co Close i odwrotnie, w tym przypadku jedno i drugie jest dobre.

Teraz, jeśli chodzi o twoje konkretne pytanie, nie, nie będziesz wyciekać pamięci.


3
„Musi” to bardzo mocne słowo. Wszędzie tam, gdzie są jakieś zasady, warto znać konsekwencje ich łamania. W przypadku MemoryStream istnieje bardzo niewiele konsekwencji.
Jon Skeet,

-1

Nie jestem ekspertem .net, ale być może problemem są tutaj zasoby, a mianowicie uchwyt pliku, a nie pamięć. Wydaje mi się, że odśmiecacz w końcu zwolni strumień i zamknie uchwyt, ale myślę, że zawsze najlepiej byłoby zamknąć go jawnie, aby upewnić się, że zawartość zostanie wyrzucona na dysk.


MemoryStream jest w całości w pamięci - nie ma tutaj uchwytu pliku.
Jon Skeet

-2

Usuwanie niezarządzanych zasobów jest niedeterministyczne w językach ze śmieciami. Nawet jeśli wywołasz Dispose jawnie, nie masz absolutnie żadnej kontroli nad tym, kiedy pamięć zapasowa zostanie faktycznie zwolniona. Dispose jest wywoływana niejawnie, gdy obiekt wychodzi poza zakres, niezależnie od tego, czy jest to wyjście z instrukcji using, czy też wywołanie stosu wywołań z metody podrzędnej. Biorąc to wszystko pod uwagę, czasami obiekt może być opakowaniem dla zarządzanego zasobu (np. Pliku). Dlatego dobrą praktyką jest jawne zamykanie instrukcji końcowych lub używanie instrukcji using. Twoje zdrowie


1
Nie do końca prawda. Dispose jest wywoływana podczas wychodzenia z instrukcji using. Dispose nie jest wywoływana, gdy obiekt po prostu wychodzi poza zakres.
Alexander Abramov

-3

MemorySteram to nic innego jak tablica bajtów, czyli obiekt zarządzany. Zapomnij o pozbyciu się lub zamknięciu, nie ma to żadnego efektu ubocznego poza nadmiernym zakończeniem finalizacji.
Po prostu sprawdź konstruktor lub metodę przepłukiwania MemoryStream w reflektorze i będzie jasne, dlaczego nie musisz martwić się o zamknięcie lub wyrzucenie go inaczej niż tylko po to, aby postępować zgodnie z dobrą praktyką.


6
-1: Jeśli zamierzasz opublikować na pytaniu dla dzieci od 4 lat z zaakceptowaną odpowiedzią, postaraj się, aby było to przydatne.
Tieson T.
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.