Встановлення об'єкта null vs Dispose ()


108

Мене захоплює те, як працюють CLR та GC (я працюю над розширенням своїх знань щодо цього, читаючи CLR через C #, книги / пости Джона Скіта тощо).

У будь-якому разі, яка різниця між висловом:

MyClass myclass = new MyClass();
myclass = null;

Або, змусивши MyClass реалізувати IDisposable і деструктор і викликати Dispose ()?

Крім того, якщо у мене є блок коду з використанням оператора (наприклад, нижче), якщо я переходжу до коду і виходжую з використовуючого блоку, чи об’єкт видаляється тоді або коли відбувається збирання сміття? Що буде, якщо я будь-коли зателефоную у розпорядження блоку Dispose ()?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

Класи потоку (наприклад, BinaryWriter) мають метод Finalize? Чому я б хотів цим скористатися?

Відповіді:


210

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

Dispose, вивезення сміття та його доопрацювання

Коли ви пишете usingзаяву, це просто синтаксичний цукор для блоку спробу / нарешті, який Disposeвикликається, навіть якщо код в тілі usingвиписки кидає виняток. Це не означає, що об’єктом є сміття, зібране в кінці блоку.

Утилізація стосується некерованих ресурсів (непам’яті). Це можуть бути ручки інтерфейсу користувача, мережеві з'єднання, ручки файлів тощо. Це обмежені ресурси, тому ви, як правило, хочете їх звільнити, як тільки зможете. Ви повинні реалізовувати, IDisposableколи ваш тип "володіє" некерованим ресурсом, безпосередньо (як правило, через IntPtr), або опосередковано (наприклад, через a Stream, SqlConnectionтощо).

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

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

Фіналізатори майже завжди використовуються для очищення ресурсів у випадку, коли користувач типу забув утилізувати їх впорядкованому порядку. Тож якщо ви відкриєте, FileStreamале забудете зателефонувати Disposeабо Close, фіналізатор згодом випустить основну ручку файлу для вас. У добре написаній програмі фіналізатори, на мою думку, майже ніколи не повинні стріляти.

Встановлення змінної в null

Одне невелике значення щодо встановлення змінної на null- це майже ніколи не потрібно для збору сміття. Іноді ви можете це зробити, якщо це змінна команда, хоча, на мій досвід, рідкість "частини" об'єкта вже не потрібна. Коли це локальна змінна, JIT, як правило, досить розумний (у режимі випуску), щоб знати, коли ви не збираєтеся знову використовувати посилання. Наприклад:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

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

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

Впровадження ID / одноразових фіналізаторів

Отже, чи повинні ваші власні типи впроваджувати фіналізатори? Майже точно не. Якщо ви лише опосередкованоFileStream володієте некерованими ресурсами (наприклад, у вас є змінною учасника), то додавання власного фіналізатора не допоможе: потік майже напевно буде придатний для збору сміття, коли ваш об’єкт є, тому ви можете просто розраховувати на FileStreamнаявність фіналізатора (при необхідності - це може стосуватися чогось іншого тощо). Якщо ви хочете безпосередньо керувати некерованим ресурсом, SafeHandleце ваш друг - це займе трохи часу, щоб розпочати роботу, але це означає, що вам майже ніколи не доведеться писати фіналізатор ще раз . Зазвичай вам потрібен фіналізатор лише у тому випадку, якщо у вас є реальна пряма обробка на ресурсі (an IntPtr), і ви хочете перейти доSafeHandle як тільки зможете. (Там є два посилання - читайте обидва, в ідеалі.)

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

Це було трохи невдало, але, будь ласка, попросіть роз'яснити, де ви хочете трохи :)


Знову "Один раз, коли, можливо, варто встановити локальну змінну на нуль" - можливо, також деякі сценарії "захоплення" терніра (декілька захоплень однієї змінної) - але, можливо, не варто ускладнювати публікацію! +1 ...
Марк Гравелл

@Marc: Це правда - я навіть не думав про захоплені змінні. Хм. Так, я думаю, що я залишу це в спокої;)
Джон Скіт

Скажіть, будь ласка, що буде, коли ви встановите "foo = null" у своєму фрагменті коду вище? Наскільки мені відомо, цей рядок очищає лише значення змінної, яка вказує на foo-об'єкт в керованій купі? тож питання в тому, що буде з об'єктом foo там? чи не повинні ми закликати розпоряджатися цим?
Одісех

@odiseh: Якщо об'єкт був одноразовим, то так - вам слід розпоряджатися ним. Цей розділ відповіді стосувався лише збирання сміття, але це зовсім окремо.
Джон Скіт

1
Я шукав роз'яснення щодо деяких проблем, що стосуються IDSposable, тому я попробовав "Скілет, що ідентифікується", і знайшов це. Чудово! : D
Мацей Возняк

22

Коли ви розпоряджаєтесь об’єктом, ресурси звільняються. Призначаючи null змінній, ви просто змінюєте посилання.

myclass = null;

Після того як ви виконаєте це, об’єкт myclass, на який посилався, все ще існує, і триватиме до тих пір, поки GC не обійдеться, щоб очистити його. Якщо Dispose буде явно викликано або знаходиться у використанні блоку, будь-які ресурси будуть звільнені якнайшвидше.


7
Він може ще не існувати після виконання цього рядка - можливо, це сміття було зібрано до цього рядка. JIT розумний - такі лінії, як це, майже завжди не мають значення.
Джон Скіт

6
Якщо встановити нуль, це може означати, що ресурси, які зберігаються об'єктом, ніколи не будуть звільнені. GC не розпоряджається, а лише доопрацьовується, тому якщо об’єкт безпосередньо містить некеровані ресурси, а його фіналізатор не розпоряджається (або у нього немає фіналізатора), то ці ресурси просочуються. Щось слід пам’ятати.
ЛукаХ

6

Дві операції не мають великого відношення один до одного. Коли ви встановлюєте посилання на null, це просто робиться так. Це саме по собі не впливає на клас, на який посилався взагалі. Ваша змінна просто більше не вказує на об'єкт, до якого вона звикла, але сам об'єкт є незмінним.

Коли ви викликаєте Dispose (), це виклик методу для самого об'єкта. Що б метод Dispose не робив, зараз робиться на об’єкті. Але це не впливає на вашу посилання на об'єкт.

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

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

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


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