Чи потрібно утилізувати предмети та встановити їх на нуль?


310

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


4
Здається, існує консенсус, що вам не потрібно встановлювати об’єкт на нуль, але чи потрібно це робити Dispose ()?
CJ7

3
як сказав Джефф: codinghorror.com/blog/2009/01/…
танатос

9
Моя порада завжди розпоряджатися, якщо об’єкт реалізує IDisposable. Кожен раз використовуйте блок використання. Не робіть припущень, не залишайте це випадковістю. Не потрібно встановлювати речі на нуль. Об'єкт просто вийшов за межі сфери.
пітер

11
@peter: Не використовуйте блоки "використання" з клієнтськими проксі-серверами: msdn.microsoft.com/en-us/library/aa355056.aspx
nlawalker

9
ЗАРАЗ, ВИ МОЖЕТЕ встановити деякі посилання на нуль всередині вашого Dispose()методу! Це тонка варіація цього питання, але важлива, оскільки об'єкт, що перебуває у розпорядженні, не може знати, чи він "виходить за межі" (дзвінок Dispose()не гарантує). Більше тут: stackoverflow.com/questions/6757048/…
Кевін П. Райс

Відповіді:


239

Об'єкти будуть очищені, коли вони більше не використовуються та коли сміттєзбірник вважає за потрібне. Іноді вам може знадобитися встановити об'єкт для nullтого, щоб він вийшов із сфери дії (наприклад, статичне поле, значення якого вам більше не потрібно), але загалом зазвичай не потрібно його встановлювати null.

Що стосується розміщення об’єктів, я погоджуюся з @Andre. Якщо об’єкт є IDisposable, хороша ідея розпоряджатися ним, коли він більше не потрібен, особливо якщо об'єкт використовує некеровані ресурси. Якщо не розпоряджатися некерованими ресурсами, це призведе до витоку пам'яті .

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

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here

Який функціонально еквівалентний:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}

4
Якщо obj є еталонним типом, то остаточний блок еквівалентний:if (obj != null) ((IDisposable)obj).Dispose();
Ренді підтримує Моніку

1
@Tuzo: Дякую! Відредаговано, щоб відобразити це.
Зак Джонсон

2
Одне зауваження щодо IDisposable. Якщо не вдалося розпорядитися об'єктом, як правило, це не спричинить витік пам'яті у будь-якому добре розробленому класі. Працюючи з керованими ресурсами в C #, у вас повинен бути фіналізатор, який все одно випустить некеровані ресурси. Це означає, що замість того, щоб розподіляти ресурси, коли це потрібно зробити, воно буде відкладено, коли сміттєзбірник доопрацює керований об’єкт. Це все ще може спричинити багато інших проблем (наприклад, невипущені замки). Вам слід розпоряджатися, IDisposableхоча!
Айдіакапі

@RandyLevy У вас є посилання на це? Спасибі
Основна

Але моє питання: Dispose () потребує впровадження будь-якої логіки? Чи потрібно щось робити? Або внутрішньо, коли Dispose () називається сигналами GC, що добре йти? Наприклад, я перевірив вихідний код TextWriter, і Dispose не має реалізації.
Михайло Георгеску

137

Об'єкти ніколи не виходять за межі в C #, як це робиться в C ++. З ними збирається сміттєзбірник автоматично, коли вони більше не використовуються. Це більш складний підхід, ніж C ++, де область змінної повністю детермінована. Збірник сміття CLR активно проходить через усі створені об’єкти та працює, якщо вони використовуються.

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

Встановлення посилань на об'єкти не nullє необхідним, оскільки збирання сміття працює шляхом розробки, на які об’єкти посилаються інші об'єкти.

На практиці ви не повинні турбуватися про знищення, це просто працює і це чудово :)

Disposeнеобхідно викликати всі об’єкти, які реалізуються, IDisposableколи ви закінчите роботу з ними. Зазвичай ви використовуєте usingблок з такими об'єктами, як:

using (var ms = new MemoryStream()) {
  //...
}

EDIT Про змінну область застосування. Крейг запитав, чи впливає змінна область впливу на термін експлуатації об'єкта. Щоб правильно пояснити цей аспект CLR, мені потрібно пояснити кілька понять із C ++ та C #.

Фактична змінна область

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

У C ++ це абсолютно законно:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8

Однак у C # ви отримуєте помилку компілятора:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}

Це має сенс, якщо подивитися на згенерований MSIL - всі змінні, які використовує функція, визначаються на початку функції. Погляньте на цю функцію:

public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}

Нижче наведено генерований ІЛ. Зауважте, що iVal2, який визначений всередині блоку if, фактично визначений на рівні функцій. Це фактично означає, що C # має лише область класу та рівня функцій, що стосується змінного терміну служби.

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope

С ++ сфера дії та термін експлуатації об'єкта

Щоразу, коли змінна C ++, виділена на стек, не виходить за межі, вона руйнується. Пам'ятайте, що в C ++ ви можете створювати об'єкти на стеці або на купі. Коли ви створюєте їх у стеку, коли виконання залишає область, вони вискакують з стека і руйнуються.

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives

Коли C ++ об’єкти створюються на купі, вони повинні бути явно знищені, інакше це витік пам'яті. Однак зі змінними стеку такої проблеми немає.

C # Термін експлуатації об'єкта

У CLR об'єкти (тобто еталонні типи) завжди створюються в керованій купі. Це додатково підкріплюється синтаксисом створення об'єкта. Розглянемо цей фрагмент коду.

MyClass stackObj;

У C ++ це створить екземпляр MyClassу стеці та викликає його конструктор за замовчуванням. У C # це створило б посилання на клас MyClass, який не вказує ні на що. Єдиний спосіб створити екземпляр класу - це використовувати newоператор:

MyClass stackObj = new MyClass();

Певним чином, об'єкти C # дуже схожі на об'єкти, створені за допомогою newсинтаксису в C ++ - вони створюються на купі, але на відміну від об'єктів C ++, ними керує час виконання, тому вам не доведеться турбуватися про їх знищення.

Оскільки об'єкти завжди знаходяться в купі, то факт, що посилання на об'єкти (тобто вказівники) виходять за межі сфери, стає суперечливим. У визначенні того, чи потрібно об'єкт збирати, існує більше факторів, ніж просто наявність посилань на об'єкт.

C # Посилання на об'єкт

Джон Скіт порівнював посилання на об'єкти на Java з фрагментами струни, які прикріплені до повітряної кулі, яка є об'єктом. Ця ж аналогія стосується посилань на об'єкти C #. Вони просто вказують на розташування купи, яка містить об'єкт. Таким чином, встановлення його на нуль не має негайного впливу на термін експлуатації об'єкта, повітряна куля продовжує існувати, поки GC "не спливає" її.

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

Об'єкти .NET дуже схожі на гелієві кулі під дахом. Коли дах відкриється (GC біжить) - невикористані повітряні кулі пливуть далеко, хоча можуть бути групи повітряних куль, які прив'язані разом.

.NET GC використовує комбінацію поколінь GC та позначення та розгортки. Підхід генерації передбачає виконання часу, що сприяє огляду об'єктів, які були виділені останнім часом, оскільки вони, швидше за все, не використовуються, а позначення та розгортка передбачає час виконання, проходячи весь графік об'єктів, і відпрацьовування, якщо є об'єкти групи, які не використовуються. Це адекватно стосується проблеми кругової залежності.

Крім того, .NET GC працює на іншому потоці (так званий фінілайзерний потік), оскільки він має зовсім небагато зробити, і це робити в основному потоці перерве вашу програму.


1
@Igor: Виходячи з "області", я маю на увазі, що посилання на об'єкт є поза контекстом і не може бути вказана в поточній області. Звичайно, це все-таки відбувається в C #.
CJ7

@Craig Johnston, не плутайте змінне масштабування, яке використовує компілятор, зі змінною тривалістю життя, яка визначається часом виконання - вони різні. Локальна змінна може бути не "живою", хоча вона все ще знаходиться в області застосування.
Ренді підтримує Моніку

1
@Craig Johnston: Див. Blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx : "немає гарантії, що локальна змінна залишатиметься активною до кінця області, якщо вона не буде Виконання може вільно проаналізувати наявний у ньому код і визначити, що немає ніяких подальших звичаїв змінної за певною точкою, а тому не підтримувати цю змінну за межами цієї точки (тобто не трактувати її як корінь для цілей ГК). "
Ренді підтримує Моніку

1
@Tuzo: Правда. Саме для цього GC.KeepAlive.
Стівен Судіт

1
@Craig Johnston: Ні і так. Ні, тому що час виконання .NET керує цим для вас і робить хорошу роботу. Так, тому що завдання програміста - не писати код, який (тільки) компілюється, а писати код, який працює . Іноді допомагає дізнатися, що виконується під кришками (наприклад, усунення несправностей). Можна стверджувати, що саме той тип знань допомагає відокремити хороших програмістів від великих програмістів.
Ренді підтримує Моніку

18

Як говорили інші, ви обов'язково хочете зателефонувати, Disposeякщо клас реалізується IDisposable. Я займаю досить жорстку позицію щодо цього. Деякі можуть стверджувати , що виклик Disposeна DataSet, наприклад, НЕ має сенсу , тому що вони розібрали його і побачив , що він нічого змістовного не робити. Але я думаю, що в цьому аргументі є помилки.

Прочитайте це для цікавої дискусії шанованих людей з цього приводу. Тоді прочитайте тут мої міркування, чому я думаю, що Джефрі Ріхтер знаходиться в неправильному таборі.

Тепер, на те, чи слід вам встановлювати посилання null. Відповідь - ні. Дозвольте проілюструвати свою думку наступним кодом.

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}

Тож коли ви вважаєте, що об'єкт, на який посилається, aпридатний для збору? Якщо ви сказали після дзвінка, a = nullто ви помиляєтесь. Якщо ви сказали, що після завершення Mainметоду, ви також помиляєтесь. Правильна відповідь полягає в тому, що він може брати участь у зборі десь під час дзвінка до DoSomething. Це правильно. Це право до встановлення посилання nullта, можливо, ще до завершення дзвінка DoSomething. Це тому, що компілятор JIT може розпізнати, коли посилання на об'єкти більше не перезаписуються, навіть якщо вони все ще вкорінені.


3
Що робити, якщо aв класі є поле приватного члена? Якщо aзначення не встановлено на нуль, GC не може знати, чи aбуде він знову використаний у якомусь методі, правда? Таким чином, aвони не будуть зібрані, поки не буде зібраний весь клас, що містить вміст. Ні?
Кевін П. Райс

4
@Kevin: Правильно. Якщо ви aбули членом класу, а клас, який містився, aвсе ще вкорінювався та використовувався, то він також завис би. Це один сценарій, коли його встановлення nullможе бути корисним.
Брайан Гедеон

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

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

13

Ніколи не потрібно встановлювати об’єкти на нуль у C #. Компілятор і час виконання подбають про те, щоб з'ясувати, коли вони вже не мають сфери застосування.

Так, вам слід розпоряджатися об’єктами, які реалізують IDisposable.


2
Якщо у вас є довгоживуча (або навіть статична) посилання на великий об’єкт, ви wantскасуєте його, як тільки закінчите з ним, щоб його можна було відшкодувати.
Стівен Судіт

12
Якщо ви коли-небудь "зробили з цим", це не повинно бути статичним. Якщо це не статично, але "довгоживуче", воно все-таки повинно вийти із сфери застосування незабаром після того, як ви закінчите це. Необхідність встановлення посилань на null вказує на проблему зі структурою коду.
EMP

Ви можете мати статичний предмет, з яким ви закінчите. Поміркуйте: статичний ресурс, який зчитується з диска у зручному для користувача форматі, а потім аналізується у форматі, придатному для використання програми. Ви можете отримати приватну копію необроблених даних, яка не виконує ніяких цілей. (Приклад із реального світу: синтаксичний аналіз - це звичайний
двопрохідний порядок,

1
Тоді він не повинен зберігати жодних необроблених даних у статичному полі, якщо вони використовуються лише тимчасово. Звичайно, ви можете це зробити, просто з цієї причини це не є хорошою практикою: тоді вам доведеться керувати його терміном експлуатації вручну.
EMP

2
Ви уникаєте цього, зберігаючи необроблені дані в локальній змінній у методі, який їх обробляє. Метод повертає оброблені дані, які ви зберігаєте, але локальні для необроблених даних виходять із сфери застосування, коли метод виходить і автоматично GCed.
EMP

11

Я погоджуюсь із загальною відповіддю тут, що так, ви повинні розпоряджатися, і ні, як правило, не слід встановлювати змінну на нуль ... але я хотів би зазначити, що dispose НЕ передусім щодо управління пам'яттю. Так, це може допомогти (а іноді і робити) з управлінням пам'яттю, але головна мета - дати детермінованому звільнення дефіцитних ресурсів.

Наприклад, якщо ви відкриєте апаратний порт (наприклад, серійний), сокет TCP / IP, файл (в режимі ексклюзивного доступу) або навіть з'єднання з базою даних, ви тепер не змогли будь-який інший код використовувати ці елементи до їх звільнення. Dispose зазвичай випускає ці елементи (разом з GDI та іншими ручками "os" тощо), яких є 1000 доступних, але загалом вони обмежені. Якщо ви не зателефонуєте розпоряджатись об'єктом власника і явно звільняєте ці ресурси, то спробуйте відкрити той самий ресурс знову (або інша програма), що спроба відкритої помилки не вдасться, тому що ваш об’єкт, що не відкрився, не збирається, все ще має відкритий елемент . Звичайно, коли GC збирає предмет (якщо схема розпорядження була реалізована правильно), ресурс буде звільнений ... але ви не знаєте, коли це буде, тому ви не ' не знаю, коли безпечно повторно відкрити цей ресурс. Це головний випуск, який розпоряджається навколо компанії Dispose. Звичайно, вивільнення цих ручок часто звільняє і пам'ять, і ніколи не випускаючи їх, ніколи не звільняє цю пам'ять ... отже, всі розмови про витоки пам'яті або затримки очищення пам'яті.

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


11

Якщо об’єкт реалізується IDisposable , то так, вам слід розпоряджатися ним. Об'єкт може зависати на власних ресурсах (файлах, ручках ОС), які не можуть бути звільнені негайно в іншому випадку. Це може призвести до голодування ресурсів, проблем із блокуванням файлів та інших тонких помилок, яких інакше можна уникнути.

Див. Також Впровадження методу розпорядження MSDN.


Але чи не зателефонує збирач сміття Dispose ()? Якщо так, то навіщо вам це називати?
CJ7

Якщо ви не зателефонуєте це прямо, немає гарантії, яка Disposeбуде закликана. Крім того, якщо ваш об’єкт тримається на дефіцитному ресурсі або блокується якийсь ресурс (наприклад, файл), ви захочете звільнити його якнайшвидше. Чекати, коли GC зробить це, неоптимально.
Кріс Шміч

12
GC ніколи не зателефонує Dispose (). GC може викликати фіналізатор, який за умовою повинен очистити ресурси.
Адріанм

@adrianm: Не mightдзвінок, а willдзвінок.
леппі

2
@leppie: фіналізатори не детерміновані і їх не можна викликати (наприклад, коли додаток завантажений). Якщо вам потрібна детермінована доопрацювання, ви повинні реалізувати те, що, на мою думку, називається критичним обробником. CLR має спеціальні поводження з цими об'єктами, щоб гарантувати їх доопрацювання (наприклад, він попередньо клацне код завершення для обробки низької пам’яті)
adrianm

9

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

EDIT: найкраще використовувати usingкоманду під час роботи з одноразовими предметами:

using(var con = new SqlConnection("..")){ ...

5

Коли об'єкт реалізує, IDisposableвам слід зателефонувати Dispose(або Close, в деяких випадках, він буде викликати розпорядження для вас).

Зазвичай вам не потрібно встановлювати об'єкти null, оскільки GC буде знати, що об’єкт більше не буде використовуватися.

Є один виняток, коли я встановлюю об'єкти null. Коли я отримую багато об’єктів (з бази даних), над якими мені потрібно працювати, і зберігаю їх у колекції (або масиві). Коли "робота" виконана, я встановлюю об'єкт null, оскільки GC не знає, що я закінчив працювати з ним.

Приклад:

using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called

4

Зазвичай немає необхідності встановлювати поля на нуль. Я завжди рекомендую розпоряджатися некерованими ресурсами, однак.

З досвіду я також радив би зробити наступне:

  • Скасуйте підписку на події, якщо вони вам більше не потрібні.
  • Встановіть будь-яке поле, в якому є делегат або вираз, на нуль, якщо це більше не потрібно.

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

Гарне місце для цього - у Dispose (), але швидше, як правило, краще.

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

Це може не бути проблемою, поки ви не виявите, що ваш додаток використовує набагато більше пам’яті, ніж ви очікували. Коли це станеться, підключіть профайл пам'яті, щоб побачити, які об’єкти не очищаються. Встановлення полів, що посилаються на інші об’єкти на нульові та очищення колекцій у розпорядженні, справді може допомогти GC розібратися, які об’єкти він може видалити з пам'яті. GC швидше поверне використану пам'ять, зробивши ваш додаток набагато меншим голодним і швидшим.


1
Що ви маєте на увазі про «події та делегатів» - що слід «очистити» від них?
CJ7

@Craig - я відредагував свою відповідь. Сподіваємось, це трохи прояснює це.
Marnix van Valen

3

Завжди дзвоніть у розпорядження. Не варто ризикувати. До великих заявок на управління підприємством слід ставитися з повагою. Ніяких припущень не можна робити, інакше це повернеться до укусу.

Не слухайте леппі.

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

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

Інструменти профілю пам'яті можуть допомогти у подібних речах, але вони можуть бути складними.

Крім того, завжди скасовуйте підписку на події, які не потрібні. Також будьте обережні з прив'язкою та контролем WPF. Не звичайна ситуація, але я натрапив на ситуацію, коли я мав контроль WPF, який прив'язувався до основного об'єкта. Основний об'єкт був великий і займав велику кількість пам'яті. Елемент керування WPF замінювався новим екземпляром, а старий чомусь все ще звисав. Це спричинило великий витік пам’яті.

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


2

Я теж повинен відповісти. JIT генерує таблиці разом з кодом зі свого статичного аналізу змінної використання. Ці записи таблиці - це "GC-Roots" у поточному кадрі стека. У міру просування вказівника інструкції ці записи в таблиці стають недійсними та настільки готовими до вивезення сміття. Тому: Якщо це масштабна змінна, вам не потрібно встановлювати її на нуль - GC збиратиме об'єкт. Якщо це член або статична змінна, ви повинні встановити її на нуль

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