Отложенная инициализация объектов

35

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

Для примера представим, что требуется создать класс, инкапсулирующий операции цифрового музыкального проигрывателя, и помимо ожидаемых методов вроде Play(), Pause() и Stop() его нужно также обеспечить способностью возврата коллекции объектов Song (через класс по имени AllTracks), которые представляют каждый из имеющихся в устройстве цифровых музыкальных файлов.

Чтобы получить такой класс, создадим новый проект типа Console Application и добавим в него следующие определения типов классов:

// Представляет одну композицию.
class Song
{
   public string Artist ( get; set; }
   public string TrackName { get; set; }
   public double TrackLength { get; set; }
}

// Представляет все композиции в проигрывателе.
class AllTracks
{
   // В нашем проигрывателе может содержаться
   // максимум 10 000 композиций.
   public AllTracks ()
   {
      // Предполагаем, что здесь производится заполнение
      // массива объектов Song.
      Console.WriteLine("Filling up the songs!");
   }
}

// Класс MediaPlayer включает объект AllTracks.
class MediaPlayer
{
   // Предполагаем, что эти методы делают нечто полезное.
   public void Play() { /* Воспроизведение композиции */ }
   public void Pause() { /* Приостановка воспроизведения композиции */ }
   public void Stop() { /* Останов воспроизведения композиции */ }
   
   private AllTracks allSongs = new AllTracks ();
   public AllTracks GetAllTracks ()
   {
      // Возвращаем все композиции.
      return allSongs;
   }
}

В текущей реализации MediaPlayer делается предположение о том, что пользователю объекта понадобится получать список объектов с помощью метода GetAllTracks(). А что если пользователю объекта не нужен этот список? Так или иначе, но переменная экземпляра AllTracks будет приводить к созданию 10 000 объектов Song в памяти:

static void Main()
(
   // В этом вызывающем коде получение всех композиций не
   // производится, но косвенно все равно создаются
   // 10 000 объектов!
   MediaPlayer myPlayer = new MediaPlayer ();
   myPlayer.Play();
   Console.ReadLine();
}

Понятно, что создания 10 000 объектов, которыми никто не будет пользоваться, лучше избежать, так как это изрядно прибавит работы сборщику мусора .NET. Хотя можно вручную добавить код, который обеспечит создание объекта allSongs только в случае его использования (например, за счет применения шаблона с методом фабрики), существует и более простой путь.

С выходом .NET 4.0 в библиотеках базовых классов появился очень интересный обобщенный класс по имени Lazy<>, который находится в пространстве имен System внутри сборки mscorlib.dll. Этот класс позволяет определять данные, которые не должны создаваться до тех пор, пока они на самом деле не начнут использоваться в кодовой базе. Поскольку он является обобщенным, при первом использовании в нем должен быть явно указан тип элемента, который должен создаваться. Этим типом может быть как любой из типов, определенных в библиотеках базовых классов .NET, так и специальный тип, самостоятельно созданный разработчиком. Для обеспечения отложенной инициализации переменной экземпляра AllTracks можно просто заменить следующий фрагмент кода:

// Класс MediaPlayer включает объект AllTracks
class MediaPlayer
{
   private AllTracks allSongs = new AllTracks ();
   public AllTracks GetAllTracks()
   {
      // Возврат всех композиций.
      return allSongs;
   }

таким кодом:

// Класс MediaPlayer включает объект Lazy<AllTracks>.
class MediaPlayer
{
   private Lazy<AllTracks> allSongs = new Lazy<AllTracks>();
   public AllTracks GetAllTracks ()
   {
      // Возврат всех композиций.
      return allSongs.Value;
   }
}

Помимо того, что переменная экземпляра AllTrack теперь имеет тип Lazy<>, важно отметить, что реализация предыдущего метода GetAllTracks() тоже изменилась. В частности, теперь требуется использовать доступное только для чтения свойство Value класса Lazy<> для получения фактических хранимых данных (в этом случае — объект AllTracks, обслуживающий 10 000 объектов Song).

Кроме того, обратите внимание, как благодаря этому простому изменению, показанный ниже модифицированный метод Main() будет незаметно размещать объекты Song в памяти только в случае, когда был действительно выполнен вызов метода GetAllTracks():

static void Main()
{
   // Никакого размещения объекта AllTracks!
   MediaPlayer myPlayer = new MediaPlayer();
   myPlayer.Play();
   
   // Размещение объекта AllTracks происходит
   // только в случае вызова метода GetAllTracks().
   MediaPlayer yourPlayer = new MediaPlayer();
   AllTracks yourMusic = yourPlayer.GetAllTracks();
   
   Console.ReadLine();
}
Пройди тесты
Лучший чат для C# программистов