Как применять IDisposable и финализаторы: 3 простых правила

Original author: Stephen Cleary
  • Translation

От переводчика


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

Как применять IDisposable и финализаторы: 3 простых правила


Документация Microsoft о применении IDisposable довольно запутанная. На самом деле она упрощается до трех простых правил.

Правило первое: не применять (до тех пор, пока это действительно не понадобится)


Реализуя интерфейс IDisposable, вы не создаете деструктор. Помните, что в среде .NET есть сборщик мусора, который работает достаточно хорошо, чтобы не присваивать null многочисленным переменным.

Существует только две ситуации, когда необходимо реализовывать IDisposable. Посмотрите на класс и определите, нужен ли вам этот интерфейс:
  • В классе есть неуправляемые ресурсы
  • В классе есть управляемые (IDisposable) ресурсы

Обратите внимание, что ресурсы должны освобождать только те классы, которым эти ресурсы принадлежат. В частности, класс может иметь ссылку на общий ресурс — в этом случае вы не должны освобождать его, поскольку другие классы могут продолжать использовать этот ресурс.

Вот пример кода, который пишут многие начинающие программисты:

// Пример неправильного применения IDisposable.
public sealed class ErrorList : IDisposable
{
    private string category;
    private List<string> errors;

    public ErrorList(string category)
    {
        this.category = category;
        this.errors = new List<string>();
    }

    // (методы добавления/отображения ошибок)

    // Совершенно необязательноpublic void Dispose()
    {
        if (this.errors != null)
        {
            this.errors.Clear();
            this.errors = null;
        }
    }
}

Некоторые программисты (особенно те, кто раньше работал с C++) идут еще дальше и добавляют финализатор:

// Пример некорректного и подверженного ошибкам применения IDisposable.
public sealed class ErrorList : IDisposable
{
    private string category;
    private List<string> errors;

    public ErrorList(string category)
    {
        this.category = category;
        this.errors = new List<string>();
    }

    // (методы добавления/отображения ошибок)

    // Совершенно необязательно
    public void Dispose()
    {
        if (this.errors != null)
        {
            this.errors.Clear();
            this.errors = null;
        }
    }

    ~ErrorList()
    {
        // Очень плохо!</font>
        // Это может вызвать исключение в потоке финализатора и нарушить работу всего приложения!</font>
        this.Dispose();
    }
}

Пример правильная реализация IDisposable для описанного класса:

// Это пример корректного применения IDisposable.
public sealed class ErrorList
{
    private string category;
    private List<string> errors;

    public ErrorList(string category)
    {
        this.category = category;
        this.errors = new List<string>();
    }
}

Все верно. Правильное применение интерфейса IDisposable для этого класса — не применять его! Когда экземпляр ErrorList становится недоступным, сборщик мусора автоматически освобождает занятую им память.

Запомните эти два критерия для применения IDisposable — класс должен владеть неуправляемыми или управляемыми ресурсами. Можно пройтись по пунктам:

1. Класс ErrorList владеет неуправляемыми ресурсами? Нет, не владеет.
2. Класс ErrorList владеет управляемыми ресурсами? Запомните, «управляемые ресурсы» — это классы, реализующие IDisposable. Проверьте каждый член класса:
      1. Класс string реализует IDisposable? Нет, не реализует.
      2. Класс List реализует IDisposable? Нет, не реализует.
      3. Если никто из членов не реализует IDisposable, класс ErrorList не владеет управляемыми ресурсами.
3. Поскольку ErrorList не владеет ни управляемыми, ни неуправляемыми ресурсами, он не требует реализации интерфейса IDisposable.

Правило второе: для класса, владеющего управляемыми ресурсами, реализуйте IDisposable (но не финализатор)


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

Реализация метода Dispose подразумевает, что: этот метод вызывается не из потока финализатора, экземпляр объекта еще не был собран сборщиком мусора и конструктор объекта успешно отработал. Эти предположения делают безопасным доступ к управляемым ресурсам.

Размещение финализатора в классе, который владеет только управляемыми ресурсами, может приводить к ошибкам. Этот пример кода может вызвать исключение в потоке финализатора и нарушит работу приложения:

// Пример неправильного и подверженного ошибкам использования финализатора.
public sealed class SingleApplicationInstance
{
    private Mutex namedMutex;
    private bool namedMutexCreatedNew;
 
    public SingleApplicationInstance(string applicationName)
    {
        this.namedMutex = new Mutex(false, applicationName, out namedMutexCreatedNew);
    }
 
    public bool AlreadyExisted
    {
        get { return !this.namedMutexCreatedNew; }
    }
 
    ~SingleApplicationInstance()
    {
        // Плохо, плохо, плохо!!!
        this.namedMutex.Close();
    }
}

Неважно, реализует ли SingleApplicationInstance интерфейс IDisposable, сам факт доступа к управляемым объектам в финализаторе — верный путь к ошибкам.

Вот пример класса, в котором отсутствует финализатор, а интерфейс IDisposable реализуется правильным, но чересчур сложным способом:

// Пример излишне сложной реализации IDisposable.
public sealed class SingleApplicationInstance : IDisposable
{
    private Mutex namedMutex;
    private bool namedMutexCreatedNew;
 
    public SingleApplicationInstance(string applicationName)
    {
        this.namedMutex = new Mutex(false, applicationName, out namedMutexCreatedNew);
    }
 
    public bool AlreadyExisted
    {
        get { return !this.namedMutexCreatedNew; }
    }
 
    // Черезчур сложно
    public void Dispose()
    {
        if (namedMutex != null)
        {
            namedMutex.Close();
            namedMutex = null;
        }
    }
}

Если класс владеет управляемыми ресурсами, он может в свою очередь вызывать у них метод Dispose. Никакого дополнительного кода не нужно. Помните, что некоторые классы переименовывают «Dispose» в «Close», поэтому реализация метода Dispose может состоять исключительно из вызовов методов Dispose и Close.

Равноценная и более простая реализация:

// Пример правильной реализации IDisposable.
public sealed class SingleApplicationInstance : IDisposable
{
    private Mutex namedMutex;
    private bool namedMutexCreatedNew;
 
    public SingleApplicationInstance(string applicationName)
    {
        this.namedMutex = new Mutex(false, applicationName, out namedMutexCreatedNew);
    }
 
    public bool AlreadyExisted
    {
        get { return !this.namedMutexCreatedNew; }
    }
 
    public void Dispose()
    {
        namedMutex.Close();
    }
}

Эта реализация метода Dispose полностью безопасна. Он может вызываться сколько угодно раз, поскольку каждая реализация IDisposable дочерних ресурсов в свою очередь безопасна. Это транзитивное свойство должно использоваться для написания подобных простых реализаций метода Dispose.

Правило третье: для класса, владеющего неуправляемыми ресурсами, реализуйте IDisposable и финализатор


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

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

Классы не должны отвечать за управляемые и неуправляемые ресурсы вместе. Написать такой класс возможно, но очень сложно сделать это правильно. Поверьте мне; лучше и не пытайтесь. Даже если в классе отсутствуют ошибки, его сопровождение превращается в кошмар. К выходу .NET 2.0 в Microsoft переписали множество классов из BCL (базовой библиотеки классов) — разделяли их на владеющие неуправляемыми и управляемыми ресурсами.

Примечание: наличие такой сложной официальной документации по IDisposable объясняется тем, что в Microsoft полагают, что ваш класс будет содержать оба типа ресурсов. Это пережиток .NET 1.0, оставленный для обратной совместимости. Даже классы, написанные в Microsoft, не следуют этому старому шаблону (они были изменены в .NET 2.0 с использованием шаблона, описанного в этой статье). FxCop будет говорить, что вам необходимо «правильно» реализовать IDisposable (т.е. использовать старый шаблон). Не слушайте его — FxCop ошибается.

Класс должен выглядеть похожим образом:

// Пример корректной реализации IDisposable.
// В идеале нужно еще наследоваться от SafeHandle.
public sealed class WindowStationHandle : IDisposable
{
    public WindowStationHandle(IntPtr handle)
    {
        this.Handle = handle;
    }
 
    public WindowStationHandle()
        : this(IntPtr.Zero)
    {
    }
 
    public bool IsInvalid
    {
        get { return (this.Handle == IntPtr.Zero); }
    }
 
    public IntPtr Handle { get; set; }
 
    private void CloseHandle()
    {
        // Если хэндл нулевой, ничего не делаем
        if (this.IsInvalid)
        {
            return;
        }
 
        // Закрытие хэндла, запись ошибок
        if (!NativeMethods.CloseWindowStation(this.Handle))
        {
            Trace.WriteLine("CloseWindowStation: " + new Win32Exception().Message);
        }

        // Установка хэндлу нулевого значения
        this.Handle = IntPtr.Zero;
    }
 
    public void Dispose()
    {
        this.CloseHandle();
        GC.SuppressFinalize(this);
    }
 
    ~WindowStationHandle()
    {
        this.CloseHandle();
    }
}
 
internal static partial class NativeMethods
{
    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool CloseWindowStation(IntPtr hWinSta);
}

В конце метода Dispose стоит вызов GC.SuppressFinalize(this). Это гарантирует, что финализатор объекта вызываться не будет.

Если же Dispose не будет вызван явно, то в конечном итоге сработает финализатор и закроет хэндл.

Метод CloseHandle сначала проверяет, является ли хэндл нулевым. Затем закрывает его, не выбрасывая возможных исключений: т.к. CloseHandle может быть вызван из финализатора, и выброс исключения приведет к остановке процесса. Завершается метод CloseHandle обнулением хэндла. Т.о. его можно будет вызывать сколько угодно раз. Это, в свою очередь, делает безопасным многократный вызов Dispose. Проверку хэндла можно было бы поместить в Dispose, однако размещение этой проверки в CloseHandle позволяет передавать нулевые хэндлы в конструктор и присваивать их свойству Handle.

Причина, по который SuppressFinalize вызывается после CloseHandle, заключается в том, что если в Dispose при закрытии хэндла произойдет ошибка, то финализатор все равно будет вызван. Эта причина подробно обсуждалась в блоге Джо Даффи (тоже очень хорошая статья, кстати — прим. пер.), хотя и является достаточно слабым аргументом. Разница существовала бы лишь в том случае, если бы метод CloseHandle при вызове из финализатора закрывал хэндл другим способом. Так делать, конечно, можно, но не рекомендуется.

Важно! Класс WindowStationHandle не содержит хэндл расположения окна и не знает ничего о создании или открытии расположения окна. Эти функции (как и другие, связанные с окнами) — задача другого класса (вероятно, «WindowStation»). Это помогает создавать корректные реализации, т.к. каждый финализатор должен выполняться даже на объектах с не до конца отработавшими из-за выброса исключения контрукторами. На практике так делать затруднительно, и это еще одна причина, почему класс-обертка должен быть разделен на класс, ответственный за закрытие хэндла, и на собственно класс-обертку.

Примечание: приведенное выше решение — самое простое, и имеет свои недостатки. Например, если поток завершается сразу после выполнения функции размещения ресурса, то может произойти утечка этого ресурса. Если вы упаковываете IntPtr-хэндл, то лучше наследоваться от класса SafeHandle. Если же вам нужно пойти дальше и поддержать надежное освобождение ресурсов, то тут все быстро становится очень запутанным (еще одна хорошая статья — прим. пер.)!
Share post

Similar posts

Comments 24

    +1
    Еще при чтении статьи у меня самого возник вопрос.

    Логика третьего правила понятна — неуправляемые ресурсы должны быть по-любому освобождены, и если это не сделает программист с помощью Dispose, то в конце концов сборщик дернет финализатор.

    А вот во втором правиле получается так, что если я Dispose забыл вызвать, то и ничего страшного?
      +2
      Ну когда сборщик мусора доберется до объекта он его таки вызовет, но иногда может быть поздно.
      Если вы в классе открыли файл для эксклюзивного доступа, а потом забыли вызывать Dispose и закрыть файл, то код, кому файл может понадобиться (например, для удаления его), может работать крайне нестабильно. В какие-то моменты сборщик успеет отработать и все будет ОК, а в какие-то вы получите ошибку.
      Поэтому лучше все же за Dispose не забывать, чтобы потом не лепить костыли вроде
      GC.Collect()
        +3
        На GC надейся, а сам не плошай!
          +3
          GC таки не вызывает Dispose. это работа программиста.
            0
            Да, действительно. Спасибо.
            Но я везде где нужно реализую IDisposable и стараюсь использовать using. И как-то был в уверенности, что в принципе это не 100% обязательно, но в реальной жизни просто крайне желательно, чтобы не ловить странные ошибки доступа и прочее.
              0
              А это так и есть — не обязательно, но крайне желательно =)
          0
          Очень даже страшно не вызывать Dispose :-)
          Используйте конструкцию using (...) {....} она сама вызовет Dispose
            0
            Зависит от ситуации =) На самом деле, просто нет никакого другого способа кроме IDisposable (ну и документации) указать пользователю класса что он может 'течь', так что создателю класса ничего не остается.
            +2
            Weak Event, теперь Dispose Pattern… интересно конечно, но имхо как-то поздно все это описывать.
              +3
              Да, замечание понятно. Но, скажем так, в любой момент найдется человек, незнакомый с таблицей умножения. Я сам, начав посещать Хабр год назад, очень много прочитал таких вот «поздних» тем и для меня они оказались очень полезны.
                +2
                Продолжайте, лучше поздно чем никогда
                  0
                  Привет, вот он я.
                  Знания медленно устаревают, а новички быстро плодятся.
                  Так что спасибо.
                    0
                    Не за что!
                0
                «Это может вызвать исключение в потоке финализатора и нарушить работу всего приложения!»

                Не понял, throwable в потоке финализатора рушит приложение? Это серьёзно так в .NET?
                  +1
                  Это так начиная с версии 2.0.
                  –1
                  IDisposable, кроме всего прочего, позволяет красиво организовывать работу с блоками кода.

                  Например следующий код:

                  using (new Log())
                  {
                  	// ...
                  	if (Services[INotifier]==null)
                  	{
                  		Log.Warn("Notifier not set!");
                  		return Log.Result(false);
                  	}
                  	else
                  	{
                  		// ...
                  	}
                  }
                  


                  Создаёт лог с таким содержимым:

                  { 2010-03-12 16:39:06Z Engine.Notify() [..\src\Engine.cs:126]
                  	! +0001 Notifier not set!
                  	= +0000 [Result]=[false]
                  } 0,0002 s / +12 288,00 b
                  


                  Просто и красиво.

                  Другой вариант полезнюшки
                  public class ForceCulture : IDisposable
                  {
                  	public ForceCulture(CultureInfo culture)
                  	{
                  		//...
                  	}
                  	public ForceCulture() : this(CultureInfo.InvariantCulture)
                  	{
                  	}
                  	//...
                  }
                  


                  В общем, идея, думаю понятна.
                  Кстати, если есть свои варианты использования — делитесь.
                    0
                    Где-то в этом же блоге была фишка с WaitCursor. В конструкторе меняешь курсор на часы, в Dispose — обратно. И оборачиваешь блок кода, на котором пользователь должен подождать, в using c этим WaitCursor'ом. Идея, думаю, понятна.
                      0
                      Использую ProgressBlock в IDisposable.
                      Изначально с marquee style.
                      В процессе можно обновлять (и отменять):

                      ///<summary>
                      ///Update progress bar
                      ///</summary>
                      ///<param name='current'>Current progress value</param>
                      ///<param name='total'>Total progress value</param>
                      ///<returns><c>False</c> for user cancel.</returns>
                      public bool UpdateProgress(long current, long total)
                      


                      IMHO лучше чем WaitCursor :)
                    0
                    Огромное спасибо за статью, я как раз сейчас изучаю .NET и после вашей статьи наконец разобрался во всех Dispose(), финализаторах и прочем.
                    До сих пор представление о них у меня было весьма туманным)
                    0
                    Первое правило не совсем верное. При разработки библиотек имеет смысл у некоторых обектов реализовывать пустой Dispose, т.к. в будущем имплементация объекта может меняться, а несчатные пользователя билиотеки врядли сразу после выхода новой версии побегут менять свой код.Но это ИМХО.
                      0
                      Спасибо за статью.
                      А как насчет наследуемых классов? Если классы наследуются и в «среднем» появляется необходимость неуправляемого ресурса.
                        0
                        В базовом классе:
                        public void Dispose()
                        {
                          Dispose(true);
                        }

                        public virtual void Dispose(bool disposing)
                        {
                          if (disposing)
                          {
                            ...
                          }
                          ...
                        }


                        * This source code was highlighted with Source Code Highlighter.

                        В наследуемых классах переопределяем Dispose с параметром, в конце вызываем базовый:
                        public override void Dispose(bool disposing)
                        {
                          if (disposing)
                          {
                           // освобождение своих ресурсов
                          }
                          base.Dispose(disposing);
                        }


                        * This source code was highlighted with Source Code Highlighter.
                          0
                          Блин, не дописал. В переопределяемом Dispose перед вызовом базового класса надо освободить неуправляемый ресурс. Также необходимо добавить финализатор и в нем вызвать Dispose(false).

                      Only users with full accounts can post comments. Log in, please.