Финализируемые и высвобождаемые типы

62

Существуют два различных подхода, которые можно применять для создания класса, способного производить очистку и освобождать внутренние неуправляемые ресурсы. Первый подход заключается в переопределении метода System.Object.Finalize() и позволяет гарантировать то, что объект будет очищать себя сам во время процесса сборки мусора (когда бы тот не запускался) без вмешательства со стороны пользователя. Второй подход предусматривает реализацию интерфейса IDisposable и позволяет обеспечить пользователя объекта возможностью очищать объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать метод Dispose(), неуправляемые ресурсы могут оставаться в памяти на неопределенный срок.

Как не трудно догадаться, оба подхода можно комбинировать и применять вместе в определении одного класса, получая преимущества от обеих моделей. Если пользователь объекта не забыл вызвать метод Dispose(), можно проинформировать сборщик мусора о пропуске финализации, вызвав метод GC.SuppressFinalize(). Если же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут финализации и получит возможность освободить внутренние ресурсы. Преимущество такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда освобождаться.

Ниже приведена версия класса MyResourceWrapper, которая предусматривает выполнение и финализации, и освобождения:

// Сложный упаковщик ресурсов.
public class MyResourceWrapper : IDisposable
{
  // Сборщик мусора будет вызывать этот метод, если
  // пользователь объекта забыл вызвать метод Dispose().
  ~MyResourceWrapper()
  {
     // Освобождение любых внутренних неуправляемых
     // ресурсов. Метод Dispose() НЕ должен вызываться
     // ни для каких управляемых объектов.
  }
  // Пользователь объекта будет вызывать этот метод для
  // того, чтобы освободить ресурсы как можно быстрее.
  public void Dispose()
  { 
     // Здесь осуществляется освобождение неуправляемых ресурсов
     // и вызов Dispose() для остальных высвобождаемых объектов.
     // Если пользователь вызвал Dispose(), то финализация не нужна,
     // поэтому далее она подавляется.
     GC.SuppressFinalize(this);
   }
}

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

Формализованный шаблон очистки

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

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

Для решения подобных вопросов с дизайном в Microsoft создали формальный шаблон очистки, который позволяет достичь оптимального баланса между надежностью, удобством в обслуживании и производительностью. Ниже приведена окончательная версия MyResourceWrapper, в которой применяется упомянутый формальный шаблон:

public class MyResourceWrapper : IDisposable
(
   // Используется для выяснения того, вызывался ли уже метод Dispose()
   private bool disposed = false;
   
   public void Dispose ()
   {
      // Вызов вспомогательного метода.
      // Значение true указывает на то, что очистка
      // была инициирована пользователем объекта.
      Cleanup(true);
      // Подавление финализации.
      GC.SuppressFinalize (this);
   }
   
   private void Cleanup(bool disposing)
   {
      // Проверка, выполнялась ли очистка,
      if (!this.disposed)
      {
          // Если disposing равно true, должно осуществляться
          // освобождение всех управляемых ресурсов,
          if (disposing)
          {
             // Здесь осуществляется освобождение управляемых ресурсов.
          }
       // Очистка неуправляемых ресурсов.
      }
      disposed = true;
   }
   
   ~MyResourceWrapper()
   {
      // Вызов вспомогательного метода.
      // Значение false указывает на то, что
      // очистка была инициирована сборщиком мусора.
      Cleanup(false);
   }
}

Обратите внимание, что в MyResourceWrapper теперь определяется приватный вспомогательный метод по имени Cleanup(). Передача ему в качестве аргумента значения true свидетельствует о том, что очистку инициировал пользователь объекта, следовательно, требуется освободить все управляемые и неуправляемые ресурсы. Когда очистка инициируется сборщиком мусора, при вызове CleanUp() передается значение false, чтобы освобождения внутренних высвобождаемых объектов не происходило (поскольку рассчитывать на то. что они по-прежнему находятся в памяти, нельзя). И, наконец, перед выходом из Cleanup() для переменной экземпляра типа bool (по имени disposed) устанавливается значение true, что дает возможность вызывать метод Dispose() много раз без появления ошибки.

После "освобождения" (dispose) объекта клиент по-прежнему может вызывать на нем какие-нибудь члены, поскольку объект пока еще находится в памяти. Следовательно, в показанном сложном классе-упаковщике ресурсов не помешало бы снабдить каждый член дополнительной логикой, которая бы, по сути, гласила: "если объект освобожден, ничего не делать, а просто вернуть управление".

Пройди тесты
Лучший чат для C# программистов