Чи почне збирач сміття IDisposable.Sispose for me?


134

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

Однак, що станеться, якщо я просто це роблю:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

і не застосовувати фіналізатор, або що-небудь інше. Чи буде рамка викликати метод Dispose для мене?

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

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

  2. Компілятор / фреймворк робить інші "магічні" речі залежно від того, які інтерфейси ви реалізуєте (наприклад: foreach, методи розширення, серіалізація на основі атрибутів тощо), тому має сенс, що це може бути і "магічним".

Хоча я читав про нього багато речей, і мається на увазі багато речей, я ніколи не зміг знайти остаточного відповіді " Так" чи "ні" на це питання.

Відповіді:


121

.Net Garbage Collector викликає метод Object.Finalize об'єкта зі збирання сміття. За замовчуванням це нічого не робить, і його потрібно перекрити, якщо ви хочете звільнити додаткові ресурси.

Розпорядження НЕ автоматично викликається і повинно бути викликом explicity, якщо потрібно звільнити ресурси, наприклад, у блоці "використання" або "спробувати остаточно"

див. http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx для отримання додаткової інформації


35
Насправді, я не вірю, що GC викликає Object.Finalize зовсім, якщо його не перекрито. Об'єкт визначено, що він фактично не має фіналізатора, а завершення придушується - що робить його більш ефективним, оскільки об’єкт не потребує черги на завершення / доступність.
Джон Скіт

7
Відповідно до MSDN: msdn.microsoft.com/en-us/library/… ви не можете насправді "переосмислити" метод Object.Finalize у C #, компілятор створює помилку: Не перекривайте об'єкт. Фіналізуйте. Замість цього надайте деструктор. ; тобто ви повинні впровадити деструктор, який ефективно виконує функції фіналізатора. [щойно тут додано для повноти, оскільки це прийнята відповідь і, швидше за все, буде прочитана]
Sudhanshu Mishra

1
GC не робить нічого для об'єкта, який не перекриває Finalizer. Він не ставиться до черги на завершення - і фіналізатор не викликається.
Дейв Блек

1
@dotnetguy - незважаючи на те, що в оригінальній специфікації C # згадується "деструктор", його насправді називають Фіналайзером - і це механіка зовсім інша, ніж те, як справжній "деструктор" працює для некерованих мов.
Дейв Блек

67

Я хочу наголосити на думці Брайана у своєму коментарі, адже це важливо.

Фіналізатори не є детермінованими деструкторами, як у C ++. Як зазначали інші, немає гарантії, коли він буде викликаний, і якщо ви маєте достатньо пам’яті, якщо він коли-небудь буде викликаний.

Але погана річ у фіналізаторах полягає в тому, що, як сказав Брайан, це призводить до того, що ваш об’єкт пережив збір сміття. Це може бути погано. Чому?

Як ви можете, чи не знаєте, GC поділяється на покоління - Gen 0, 1 і 2, а також Велика купка об'єктів. Спліт - це вільний термін - ви отримуєте один блок пам'яті, але є покажчики того, де починаються і закінчуються об'єкти Gen 0.

Процес думки полягає в тому, що ви, ймовірно, будете використовувати багато об'єктів, які будуть недовговічними. Тому GC має легко та швидко дістатись до об'єктів Gen 0. Отже, коли є тиск в пам'яті, перше, що це робиться, - це колекція Gen 0.

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

Це означає, що якщо ви робите щось подібне:

~MyClass() { }

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

Отже, все це лише донести додому точку, що ви хочете використовувати IDisposable, щоб очистити ресурси, коли це можливо, і серйозно спробувати знайти шляхи використання фіналізатора. Це в інтересах вашої програми.


8
Я погоджуюся, що ви хочете використовувати IDisposable, коли це можливо, але ви також повинні мати фіналізатор, який викликає метод розпорядження. Ви можете зателефонувати GC.SuppressFinalize () в IDispose.Dispose після виклику методу розпорядження, щоб ваш об'єкт не потрапив у чергу фіналізатора.
jColeson

2
Покоління пронумеровані 0-2, а не 1-3, але ваш пост інакше хороший. Я хотів би додати до цього, що будь-які об'єкти, на які посилається ваш об’єкт, або будь-які об'єкти, на які посилаються ці тощо, також будуть захищені від збору сміття (хоча і не від доопрацювання) для іншого покоління. Таким чином, об'єкти з фіналізаторами не повинні містити посилання на що-небудь, не потрібне для доопрацювання.
supercat


3
Щодо "Ваш об'єкт, незважаючи ні на що, житиме до Покоління 2." Це ДУЖЕ фундаментальна інформація! Це заощадило багато часу на налагодження системи, де було багато короткотривалих об'єктів Gen2, "підготовлених" для доопрацювання, але ніколи не доопрацьованих спричинено OutOfMemoryException через велике використання купи. Видаливши (навіть порожній) фіналізатор і перемістивши (обійшовши) код в інше місце, проблема зникла і GC змогла впоратися з навантаженням.
точилка

@CoryFoy "Ваш об'єкт, незважаючи ні на що, буде жити до Покоління 2" Чи є на це документація?
Ашиш Негі

33

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

  • Колекціонер сміття ніколи безпосередньо не виконує метод утилізації для вас.
  • GC буде виконувати фіналізатор , коли він відчуває , як він.
  • Однією загальною схемою, яка використовується для об'єктів, які мають фіналізатор, є те, щоб він викликав метод, який за умовою визначається як Dispose (bool disposition), передаючи помилковий, щоб вказати, що виклик був здійснений завдяки завершенню, а не явному виклику Dispose.
  • Це пояснюється тим, що під час доопрацювання об'єкта не можна робити припущення щодо інших керованих об'єктів (вони, можливо, вже були доопрацьовані).

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

Це проста версія, але є маса нюансів, які можуть спонукати вас за цією схемою.

  • У договорі на IDisposable.Dispose вказується, що повинно бути безпечно дзвонити кілька разів (дзвінок розпоряджатися об’єктом, який уже був розміщений, нічого не повинен робити)
  • Правильно керувати ієрархією спадкування одноразових об'єктів може бути дуже складним, особливо якщо різні шари впроваджують нові одноразові та некеровані ресурси. У наведеному вище шаблоні Dispose (bool) є віртуальним, щоб дозволити його переосмислити, щоб ним можна було керувати, але я вважаю, що він схильний до помилок.

На мою думку, набагато краще повністю уникати будь-яких типів, які безпосередньо містять як одноразові посилання, так і власні ресурси, які можуть потребувати доопрацювання. SafeHandles забезпечують дуже чистий спосіб зробити це шляхом інкапсуляції власних ресурсів у одноразові, які внутрішньо забезпечують їх власну доопрацювання (разом із низкою інших переваг, як, наприклад, видалення вікна під час P / Invoke, де натисна ручка може бути втрачена через асинхронний виняток) .

Просте визначення SafeHandle робить це дрібницею:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

Дозволяє спростити вміст, що містить:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

1
Звідки береться клас SafeHandleZeroOrMinusOneIsInvalid? Це вбудований тип .net?
Оріон Едвардс

+1 для // На мою думку, набагато краще повністю уникати будь-яких типів, які безпосередньо містять як одноразові посилання, так і власні ресурси, які можуть вимагати доопрацювання. доопрацювання.
supercat

1
@OrionEdwards так дивіться msdn.microsoft.com/en-us/library/…
Martin Capodici

1
Що стосується заклику до GC.SuppressFinalizeцього прикладу. У цьому контексті SuppressFinalize слід викликати лише у тому випадку, коли воно Dispose(true)виконується успішно. Якщо Dispose(true)в якийсь момент після фіналізації припиняється, але до того, як всі ресурси (особливо некеровані) будуть очищені, ви все одно хочете, щоб відбулася фіналізація, щоб зробити якомога більше очищення. Краще перенести GC.SuppressFinalizeвиклик у Dispose()метод після виклику на Dispose(true). Дивіться Рамкові рекомендації щодо дизайну та цей пост .
BitMask777

6

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


EDIT: Я пішов і випробував, щоб переконатися:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

Робити припущення щодо об'єктів, які вам доступні під час утилізації, може бути небезпечно і хитро, особливо під час доопрацювання.
Скотт Дорман

3

Не у випадку, який ви описуєте, але GC зателефонує вам Finalizer , якщо у вас є.

ЗАРАЗ. Наступний збір сміття, замість того, щоб бути зібраним, об’єкт перейде в чергу доопрацювання, все збирається, тоді його називають фіналізатором. Наступна колекція після цього буде звільнена.

Залежно від тиску пам’яті у вашій програмі, ви, можливо, не матимете gc для цього покоління об’єктів. Тож у випадку, скажімо, потоку файлів або db-з'єднання, можливо, доведеться почекати деякий час, щоб визволений некерований ресурс у виклику фіналізатора деякий час викликав деякі проблеми.


1

Ні, це не називається.

Але це дозволяє легко не забувати розпоряджатися своїми предметами. Просто використовуйте usingключове слово.

Для цього я зробив наступний тест:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }

1
Це був приклад того, як якщо ви НЕ використовуєте ключове слово <code> using </code>, воно не буде називатися ... і цей фрагмент має 9 років, з днем ​​народження!
пеняскіто

1

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

Дивіться цю статтю для обговорення найкращого способу впоратися з цим.


0

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


0

Шаблон IDisposable був створений головним чином для виклику розробника, якщо у вас є об'єкт, який реалізує IDispose, розробник повинен або реалізувати usingключове слово навколо контексту об'єкта, або викликати метод Dispose безпосередньо.

Безпечним відмовою для шаблону є реалізація фіналізатора, що викликає метод Dispose (). Якщо цього не зробити, ви можете створити деякі витоки пам’яті, тобто: Якщо ви створили COM-обгортку і ніколи не викликаєте System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (який буде розміщений у методі Dispose).

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

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