Будет ли сборщик мусора вызывать IDisposable. Утилизировать для меня?

134

.NET IDisposable Pattern подразумевает, что если вы пишете финализатор и внедряете IDisposable, ваш финализатор должен явно вызывать Dispose. Это логично, и это то, что я всегда делал в тех редких ситуациях, когда требуется финализатор.

Однако, что произойдет, если я просто сделаю это:

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

и не реализовывать финализатор или что-то еще. Будет ли фреймворк вызывать метод Dispose для меня?

Да, я понимаю, что это звучит глупо, и вся логика подразумевает, что этого не произойдет, но у меня всегда было 2 вещи в затылке, которые заставляли меня неуверенно себя чувствовать.

  1. Кто-то несколько лет назад однажды сказал мне, что он на самом деле это сделает, и у этого человека был очень солидный послужной список "знания своих вещей".

  2. Компилятор / фреймворк выполняет другие «волшебные» вещи в зависимости от того, какие интерфейсы вы реализуете (например, foreach, методы расширения, сериализация на основе атрибутов и т. Д.), Поэтому имеет смысл, что это тоже может быть «магическим».

Несмотря на то, что я прочитал много материала об этом, и многое подразумевалось, я так и не смог найти окончательного ответа «да» или «нет» на этот вопрос.

Орион Эдвардс
источник

Ответы:

121

Сборщик мусора .Net вызывает метод Object.Finalize объекта для сборки мусора. По умолчанию это ничего не делает и должно быть переопределено, если вы хотите освободить дополнительные ресурсы.

Dispose НЕ вызывается автоматически и должен быть вызван явным образом, если ресурсы должны быть освобождены, например, в блоке «using» или «try finally»

см. http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx для получения дополнительной информации

Xian
источник
35
На самом деле, я не верю, что GC вообще вызывает Object.Finalize, если он не переопределен. Объект определенно не имеет финализатора, и финализация подавляется - что делает его более эффективным, поскольку объект не должен находиться в очереди завершения / свободной памяти.
Джон Скит
7
Согласно MSDN: msdn.microsoft.com/en-us/library/… вы не можете «переопределить» метод Object.Finalize в C #, компилятор генерирует ошибку: не переопределяет object.Finalize. Вместо этого предоставьте деструктор. ; т.е. вы должны реализовать деструктор, который эффективно действует как финализатор. [просто добавлено здесь для полноты, так как это принятый ответ и, скорее всего, будет прочитан]
Судханшу Мишра
1
GC ничего не делает с объектом, который не перекрывает Finalizer. Он не помещается в очередь Финализации - и Финализатор не вызывается.
Дейв Блэк,
1
@dotnetguy - хотя в оригинальной спецификации C # упоминается «деструктор», его на самом деле называют «финализатором» - и его механика полностью отличается от того, как работает настоящий «деструктор» для неуправляемых языков.
Дейв Блэк,
67

Я хочу подчеркнуть точку зрения Брайана в его комментарии, потому что это важно.

Финализаторы не являются детерминированными деструкторами, как в C ++. Как уже отмечали другие, нет гарантии того, когда он будет вызван, и действительно, если у вас достаточно памяти, будет ли он когда-либо вызван.

Но плохая вещь в финализаторах заключается в том, что, как сказал Брайан, это заставляет ваш объект переживать сборку мусора. Это может быть плохо. Зачем?

Как вы можете знать, а может и не знать, GC разделен на поколения - Gen 0, 1 и 2 плюс куча больших объектов. Разделение - это бесполезный термин - вы получаете один блок памяти, но есть указатели того, где объекты Gen 0 начинаются и заканчиваются.

Мысленный процесс состоит в том, что вы, вероятно, будете использовать множество объектов, которые будут недолговечны. Так что GC должен легко и быстро добраться до объектов Gen 0. Поэтому, когда возникает нехватка памяти, первое, что он делает, это коллекция Gen 0.

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

Это означает, что если вы делаете что-то вроде этого:

~MyClass() { }

Ваш объект, несмотря ни на что, доживет до поколения 2. Это потому, что GC не может вызвать финализатор во время сборки мусора. Таким образом, объекты, которые должны быть завершены, перемещаются в специальную очередь, которая будет очищена другим потоком (потоком финализатора - который, если вы убьете, приведет к возникновению всевозможных плохих вещей). Это означает, что ваши объекты задерживаются дольше и потенциально вызывают больше мусора.

Таким образом, все это просто указывает на то, что вы хотите использовать IDisposable для очистки ресурсов, когда это возможно, и серьезно пытаться найти способы обойти использование финализатора. Это в интересах вашего приложения.

Кори Фой
источник
8
Я согласен, что вы хотите использовать IDisposable всякий раз, когда это возможно, но у вас также должен быть финализатор, который вызывает метод dispose. Вы можете вызвать GC.SuppressFinalize () в IDispose.Dispose после вызова метода dispose, чтобы убедиться, что ваш объект не помещен в очередь финализатора.
jColeson
2
Поколения нумеруются 0-2, а не 1-3, но ваш пост в остальном хорош. Однако я хотел бы добавить, что любые объекты, на которые ссылается ваш объект, или любые объекты, на которые они ссылаются и т. Д., Также будут защищены от сборки мусора (хотя и не от завершения) для другого поколения. Таким образом, объекты с финализаторами не должны содержать ссылок на то, что не нужно для финализации.
суперкат
3
Относительно "Ваш объект, несмотря ни на что, доживет до Поколения 2". Это ОЧЕНЬ фундаментальная информация! Это сэкономило много времени на отладку системы, в которой было много короткоживущих объектов Gen2, «подготовленных» к финализации, но никогда не завершенных, что вызвало OutOfMemoryException из-за интенсивного использования кучи. После удаления (даже пустого) финализатора и перемещения (обхода) кода в другое место проблема исчезла, и сборщик мусора смог справиться с нагрузкой.
точилка
@CoryFoy "Ваш объект, несмотря ни на что, доживет до поколения 2" Есть ли какая-либо документация для этого?
Ашиш Неги
33

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

  • Сборщик мусора никогда не выполнит для вас метод Dispose.
  • GC выполнит финализаторы, когда захочет .
  • Один общий шаблон, который используется для объектов, имеющих финализатор, - это вызов его метода, который по соглашению определен как Dispose (удаление bool), передавая false, чтобы указать, что вызов был сделан из-за финализации, а не явного вызова 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 для объекта, который уже был удален, ничего не должен делать)
  • Правильно управлять иерархией наследования одноразовых объектов может быть очень сложно, особенно если разные уровни представляют новые одноразовые и неуправляемые ресурсы. В приведенном выше шаблоне 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 за // На мой взгляд, гораздо лучше полностью избегать использования типов, которые напрямую содержат как одноразовые ссылки, так и нативные ресурсы, которые могут потребовать завершения. финализации.
Суперкат
1
@OrionEdwards: да, см. Msdn.microsoft.com/en-us/library/…
Мартин Каподичи
1
Относительно вызова GC.SuppressFinalizeв этом примере. В этом контексте SuppressFinalize следует вызывать только в случае Dispose(true)успешного выполнения. Если Dispose(true)в какой-то момент происходит сбой после подавления финализации, но до очистки всех ресурсов (особенно неуправляемых), вам все равно нужно выполнить финализацию, чтобы выполнить как можно больше очистки. Лучше переместить GC.SuppressFinalizeвызов в Dispose()метод после вызова Dispose(true). См. Руководство по разработке фреймворка и этот пост .
BitMask777
6

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


РЕДАКТИРОВАТЬ: Я ушел и проверил, просто чтобы убедиться:

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 позвонит вам в финализатор , если он у вас есть.

ТЕМ НЕ МЕНИЕ. Следующая сборка мусора, вместо того, чтобы собираться, объект попадает в очередь финализации, все собирается, затем вызывается финализатор. Следующая коллекция после этого будет освобождена.

В зависимости от нагрузки на память вашего приложения, у вас может не быть gc для генерации этого объекта некоторое время. Таким образом, в случае, скажем, файлового потока или соединения БД, вам, возможно, придется подождать некоторое время, пока неуправляемый ресурс на некоторое время будет освобожден в вызове финализатора, что вызовет некоторые проблемы.

Брайан Лихи
источник
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!");
    }
penyaskito
источник
1
Это был пример того, как, если вы НЕ используете ключевое слово <code> using </ code>, оно не будет называться ... и этому фрагменту исполняется 9 лет, с днем ​​рождения!
penyaskito
1

GC не будет звонить утилизировать. Он может вызвать ваш финализатор, но даже это не гарантируется при любых обстоятельствах.

Смотрите эту статью для обсуждения лучшего способа справиться с этим.

Роб Уокер
источник
0

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

Джозеф Дейгл
источник
0

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

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

В clr нет магии для автоматического вызова методов Dispose, кроме отслеживания объектов, содержащих финализаторы, и их сохранения в таблице финализаторов GC и вызова их, когда GC запускает некоторую очистку эвристики.

Эрик Сгарби
источник