Чи створюється витік пам'яті, якщо MemoryStream в .NET не закритий?


112

У мене є такий код:

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

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

Чи є ймовірність, що виділений я MemoryStream якось не вдасться позбавитись згодом?

Я маю експертну оцінку, наполягаючи на тому, що я закриваю це вручну, і я не можу знайти інформацію, щоб сказати, чи має він дійсну точку чи ні.


41
Запитайте рецензента, чому він вважає, що ви повинні закрити його. Якщо він говорить про загальну хорошу практику, він, ймовірно, розумний. Якщо він раніше говорить про звільнення пам'яті, він помиляється.
Джон Скіт

Відповіді:


60

Якщо щось є одноразовим, завжди слід розпоряджатися цим. Ви повинні використовувати usingоператор у своєму bar()методі, щоб переконатися, що ви ms2отримаєте розпорядження.

Врешті-решт очищення сміттєзбірника буде очищене, але завжди добре називати утилізацію. Якщо ви запустите FxCop на своєму коді, він позначить його як попередження.


16
За допомогою блокових дзвінків ви можете розпоряджатися.
Нік

20
@Grauenwolf: ваше твердження порушує інкапсуляцію. Як споживача, вам не варто байдуже, чи не є опціоном: якщо він ідентифікується, це ваша робота розпоряджатися ().
Марк Гравелл

4
Це не вірно для класу StreamWriter: він розпоряджатиметься підключеним потоком лише у тому випадку, якщо ви розпоряджаєтеся StreamWriter - він ніколи не розпоряджатиметься потоком, якщо він збирає сміття та називається його фіналізатор - це за задумом.
springy76

4
Я знаю, що це питання було з 2008 року, але сьогодні ми маємо бібліотеку завдань .NET 4.0. Утилізуйте () у більшості випадків непотрібно під час використання Завдання. Хоча я погоджуюся, що IDisposable має означати "Ви краще розпоряджайтесь цим, коли закінчите", це насправді це ще не означає.
Філ

7
Ще один приклад того, що вам не слід розпоряджатись об’єктом, що використовує ID, є HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong Просто ще один приклад з BCL, де є об'єкт, що не використовується, і вам не потрібно (або навіть не слід) розпоряджатися це. Це лише для того, щоб пам’ятати, що зазвичай є деякі винятки із загального правила, навіть у БКЛ;)
Маріуш Павельський

166

Ви нічого не просочите - принаймні в поточній реалізації.

Calling Dispose швидше не очистить пам'ять, яку використовує MemoryStream. Це не зупинить ваш потік від життєздатності для читання / запису дзвінків після дзвінка, що може вам бути корисним.

Якщо ви абсолютно впевнені, що ніколи не хочете переходити з MemoryStream до іншого виду потоку, це не принесе вам шкоди, щоб не викликати Dispose. Тим НЕ менше, це взагалі хороша практика почасти тому , що якщо ви коли - небудь зробити зміни , щоб використовувати інший потік, ви не хочете , щоб отримати вкусили помилкою важкодоступній знахідки , тому що ви вибрали легкий шлях рано. (З іншого боку, є аргумент YAGNI ...)

Інша причина зробити це все-таки полягає в тому, що нова реалізація може впровадити ресурси, які були б звільнені на Dispose.


У цьому випадку функція повертає MemoryStream, оскільки вона надає "дані, які можна інтерпретувати по-різному залежно від параметрів виклику", тому це міг бути байтовий масив, але з інших причин було легше зробити це як MemoryStream. Тож це точно не буде іншим класом Stream.
Coderer

У такому випадку я б все-таки намагався розпоряджатися цим принципом лише на загальному принципі - будуйте добрі звички тощо - але я б не переживав надто сильно, якби це стало хитро.
Джон Скіт

1
Якщо хтось дійсно переживає за звільнення ресурсів як можна швидше, скасуйте посилання відразу після блоку "використання", тому невправлені ресурси (якщо такі є) очищаються, і об'єкт стає придатним для вивезення сміття. Якщо метод одразу повертається, він, ймовірно, не матиме великої різниці, але якщо ви продовжуєте робити інші речі методу, такі як запит на додаткову пам’ять, то, безумовно, це може змінити значення.
Трайнко

@Triynko Не дуже правда: для детальної інформації дивіться: stackoverflow.com/questions/574019/… .
Джордж Стокер

10
Аргумент ЯГНІ можна сприймати обома способами - оскільки вирішити не розпоряджатися тим, що реалізується IDisposable, це особливий випадок, що суперечить звичайній кращій практиці, ви можете стверджувати, що це той випадок, якого вам не слід робити, поки вам справді не потрібно в рамках ЯГНІ принцип.
Джон Ханна

26

Так, є витік , залежно від того, як ви визначаєте ЛІК та скільки ПОСЛІД маєте на увазі ...

Якщо під протіканням ви маєте на увазі "пам’ять залишається виділеною, недоступною для використання, навіть якщо ви її використовуєте", а під останнім ви маєте на увазі в будь-який час після виклику розпоряджатися, то так, може виникнути витік, хоча він не є постійним (тобто для термін служби ваших програм).

Щоб звільнити керовану пам'ять, яку використовує MemoryStream, потрібно скасувати її , скасувавши свою посилання на неї, щоб вона одразу стала придатною для збору сміття. Якщо цього не зробити, тоді ви створюєте тимчасовий витік з часу, коли ви закінчите його використання, поки ваша посилання не вийде за межі, оскільки тим часом пам'ять буде недоступною для розподілу.

Перевага використовуючого оператора (над просто викликом розпоряджається) полягає в тому, що ви можете ДЕКЛАРИРУВАТИ свою посилання в використовуваному операторі. Після завершення використання оператора не тільки вилучається, але і ваша посилання виходить за межі, фактично зводить нанівець посилання та робить ваш об'єкт придатним для збору сміття негайно, не вимагаючи від вас пам’ятати про написання коду «reference = null».

Хоча відмова від невиконання чогось відразу не є класичним "постійним" витоком пам'яті, це, безумовно, має той же ефект. Наприклад, якщо ви зберігаєте посилання на MemoryStream (навіть після виклику розпорядження) і трохи далі в своєму способі, ви намагаєтеся виділити більше пам'яті ... пам'ять, якою використовується ваш все ще посилається потік пам'яті, не буде доступною для вас, поки ви не зведете нанівець посилання або воно не вийде за межі, навіть якщо ви зателефонували утилізувати і виконаєте його використання.


6
Я люблю цю відповідь. Іноді люди забувають про подвійний обов'язок використання: прагнення до рекультивації ресурсів та прагнення перенаправити.
Кіт

1
Дійсно, хоча я чув, що на відміну від Java, компілятор C # виявляє "останнє можливе використання", тому, якщо змінній призначено вийти з сфери застосування після останньої посилання, вона може отримати право на збирання сміття відразу після останнього можливого використання. перед тим, як вона фактично виходить із сфери застосування. Дивіться stackoverflow.com/questions/680550/explicit-nulling
Triynko

2
Збір сміття та тремтіння не працюють так. Область застосування - це мовна конструкція, а не те, що виконуватимуться під час виконання. Насправді ви, ймовірно, подовжуєте час, коли посилання знаходиться в пам'яті, додаючи виклик до .Dispose (), коли блок закінчується. Дивіться ericlippert.com/2015/05/18/…
Пабло Монтілья

8

Зателефонувати .Dispose()(або завершити з ним Using) не потрібно.

Причина, яку ви телефонуєте, .Dispose()- звільнити ресурс якомога швидше .

Подумайте з точки зору, наприклад, сервера переповнення стека, де у нас обмежений набір пам'яті та тисячі запитів, що надходять. для нових вхідних запитів.


24
Виклик розпорядження на MemoryStream, однак, не вивільняє жодної пам'яті. Насправді ви все ще можете отримати дані в MemoryStream після того, як ви зателефонували Dispose - спробуйте :)
Джон Скіт

12
-1 Хоча це правда для MemoryStream, загальна порада - це просто неправильно. Розпорядження полягає у випуску некерованих ресурсів, таких як ручки файлів або підключення до бази даних. Пам'ять не належить до цієї категорії. Вам майже завжди слід чекати, коли заплановане збирання сміття звільнить пам'ять.
Джо

1
Яка перевага прийняття одного стилю кодування для розподілу та розпорядження FileStreamоб'єктами та іншого для MemoryStreamоб'єктів?
Роберт Россні

3
FileStream включає некеровані ресурси, які фактично можуть бути негайно звільнені після виклику розпорядження. З іншого боку, MemoryStream зберігає керований байтовий масив у своїй змінній _buffer, яка не звільняється під час утилізації. Насправді, _buffer навіть не зведений на нуль у методі Dispose MemoryStream, який є ВІДМОВИМ ІМО BUG, ​​тому що, якщо нулювання посилання може зробити пам'ять правом на отримання права доступу до часу GC. Натомість поміркована (але розміщена) посилання MemoryStream все ще зберігається в пам'яті. Тому, як тільки ви розпоряджаєтесь нею, вам слід також визнати його недійсним, якщо воно все ще є в обсязі.
Трайко

@Triynko - "Тому, як тільки ви розпоряджаєтесь нею, вам також слід скасувати її, якщо вона все ще є в області", - я не погоджуюся. Якщо він буде використаний знову після виклику Dispose, це спричинить NullReferenceException. Якщо після Dispose це не використовується знову, немає необхідності скасовувати його; GC досить розумний.
Джо

8

На це вже відповіли, але я просто додам, що хороший старомодний принцип приховування інформації означає, що ви можете в якийсь майбутній момент захотіти переробити:

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

до:

Stream foo()
{    
   ...
}

Це підкреслює, що абонентам не слід дбати про те, який тип потоку повертається, і дає можливість змінити внутрішню реалізацію (наприклад, під час знущання щодо тестування одиниць).

Тоді у вас виникнуть проблеми, якщо ви не використовували Dispose у своїй барі:

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

5

Усі потоки реалізують IDisposable. Оберніть свій потік пам’яті в використовуваному операторі, і ви будете чудовий і веселий. Блок, що використовує, забезпечить, щоб ваш потік був закритий і розміщений незалежно від того.

де б ви не зателефонували до Foo, ви можете зробити це за допомогою (MemoryStream ms = foo ()), і я думаю, вам все одно все в порядку.


1
Одна з проблем, з якими я зіткнулася з цією звичкою, - ви повинні бути впевнені, що потік не використовується більше ніде. Наприклад, я створив JpegBitmapDecoder, який вказував на MemoryStream і повернув Frames [0] (думаючи, що він скопіює дані у власний внутрішній сховище), але виявив, що растрова карта відображатиметься лише 20% часу - виявилося, що це було Я розпоряджався потоком пам’яті.
devios1

Якщо ваш потік пам'яті повинен зберігатися (тобто використання блоку не має сенсу), слід зателефонувати Dispose і негайно встановити змінну на null. Якщо ви розпоряджаєтесь, то це більше не призначене для використання, тому вам слід також встановити його на нуль відразу. Те, що описує Чагуй, звучить як питання управління ресурсами, тому що ви не повинні передавати посилання на щось, якщо тільки те, що ви йому передаєте, не несе відповідальність за його розпорядження, а те, що передає посилання, знає, що воно більше не несе відповідальності за роблячи так.
Трайко

2

Ви не будете просочувати пам'ять, але ваш переглядач кодів правильно вказав, що слід закрити потік. Ввічливо це зробити.

Єдина ситуація, в якій ви можете просочити пам'ять - це випадково залишити посилання на потік і ніколи не закрити його. Ви все ще не дійсно витоку пам'яті, але які без необхідності розширення кількості часу , яке ви стверджуєте використовувати його.


1
> Ви все ще не дуже просочуєтеся пам’яттю, але ви зайво збільшуєте кількість часу, за яке ви заявляєте, що нею користуєтесь. Ти впевнений? Утилізація не звільняє пам'ять, а виклик її у функцію запізнення може фактично продовжити час її неможливості збирати.
Джонатан Аллен

2
Так, у Джонатана є крапка. Виконання виклику вчасно розпоряджатись функцією може фактично змусити компілятора думати, що вам потрібно зайти в екземпляр потоку (закрити його) дуже пізно у функції. Це може бути гірше, ніж взагалі не викликати розпорядження (отже, уникати затримки посилань на функцію на змінну потоку), оскільки компілятор може інакше обчислити оптимальну точку випуску (він же "точка останнього можливого використання") раніше у функції .
Трайко

2

Я б рекомендував обгортання MemoryStream в bar()в usingзаяві , головним чином , для послідовності:

  • Зараз MemoryStream не звільняє пам'ять .Dispose() , але можливо, в якийсь момент в майбутньому це може виникнути, або ви (або хтось інший у вашій компанії) може замінити його на ваш власний користувальницький MemoryStream, який працює тощо.
  • Це допомагає встановити схему у вашому проекті, щоб забезпечити утилізацію всіх потоків - лінія більш чітко намальована, кажучи "всі потоки повинні бути утилізовані", а не "деякі потоки повинні бути утилізовані, але певні не повинні". ...
  • Якщо ви коли-небудь змінюєте код, щоб дозволити повернення інших типів потоків, вам потрібно буде змінити його, щоб все-таки розпоряджатися.

Інша річ, яку я зазвичай роблю у таких випадках, як foo()при створенні та поверненні ідентифікатора, - це переконатися, що будь-який збій між побудовою returnоб'єкта та вилученим винятком викидає об'єкт та повторно виключає виняток:

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

Якщо об’єкт реалізує IDisposable, ви повинні зателефонувати до методу .Dispose, коли закінчите.

У деяких об'єктах "Утилізація" означає те саме, що "Закрити", і навпаки, у цьому випадку або добре.

Тепер, для вашого конкретного питання, ні, ви не просочите пам'ять.


3
«Обов’язково» - дуже сильне слово. Щоразу, коли є правила, варто знати наслідки їх порушення. Для MemoryStream наслідків дуже мало.
Джон Скіт

-1

Я не експерт .net, але, мабуть, проблема тут - ресурси, а саме обробка файлів, а не пам'ять. Я думаю, що збирач сміття врешті-решт звільнить потік і закриє ручку, але я думаю, що завжди було б найкращим способом закрити це явно, щоб переконатись, що ви вимили вміст на диск.


MemoryStream - це все в пам'яті - тут немає файлової обробки.
Джон Скіт

-2

Утилізація некерованих ресурсів є недетермінованою на зібраних сміттям мовах. Навіть якщо ви чітко зателефонуєте у розпорядження, ви не маєте абсолютно ніякого контролю над тим, коли резервна пам'ять фактично звільнена. Dispose неявно називається, коли об’єкт виходить із сфери застосування, будь то шляхом виходу із використовуючого оператора чи спливаючої позиції callsta з підпорядкованого методу. Все це говориться, іноді об'єкт може бути фактично обгорткою керованого ресурсу (наприклад, файлу). Ось чому є хорошою практикою явно закривати остаточні висловлювання або використовувати оператор, що використовує. Ура


1
Не зовсім правда. Dispose викликається при виході з використання оператора. Утилізація не називається, коли об’єкт просто виходить із сфери застосування.
Олександр Абрамов

-3

MemorySteram - це не що інше, як масив байтів, яким керує об'єкт. Забудьте розпоряджатися або закривати це не має жодного побічного ефекту, ніж над головою доопрацювання.
Просто перевірте конвектор або метод промивання MemoryStream у рефлекторі, і буде зрозуміло, чому вам не потрібно турбуватися про його закриття чи утилізацію, окрім як лише для того, щоб дотримуватися належної практики.


6
-1: Якщо ви збираєтесь розмістити запитання на 4 + річні роки з прийнятою відповіддю, будь ласка, спробуйте зробити це щось корисне.
Тісон Т.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.