Lazy Computation in C# (Ленивые вычисления в C#)

Немного теории.

Большинство современных языков разработки, используемых на практике (таких как C#, VB.NET, C++, Python и Java) реализуют так называемые немедленные вычисления, это означает, что операция выполняется, так только становятся известны значения её операндов. Однако, ясно, что немедленное вычисление многих функций не всегда необходимо и рационально с точки зрения производительности, поэтому само собой напрашивается решение, позволяющее отложить эти вычисления на тот момент, когда они нам будут действительно нужны.

Мартин Фаулер в свой книге PoEAA вводит понятие паттерна Lazy Load (загрузка по требованию, ленивая загрузка), суть которого состоит в том, что объект не содержит данные, но знает где их взять, если они ему станут нужны. Это как раз то, о чём мы и ведем речь, следовательно воспользуемся этим шаблоном.

Реализовать данный шаблон можно несколькими различными вариантами:

  1. Lazy Initialization – Инициализация по требованию. Это самый простой способ – реализовать проверку поля на null и в случае необходимости заполнять его данными.
  2. Virtual Proxy – Виртуальный прокси-объект. Метод несколько усложнен проблемой идентификации объектов, т.к. вместо них, до инициализации, выступают заменители.
  3. Ghost – Фиктивный объект, Призрак. Это реальный объект с неполным состоянием.
  4. Value Holder – Диспетчер значения. Объект является оболочкой для некоторого значения. Так же не самый лучший вариант в связи с проблемами типизации.

Примеры реализации.

В рассмотренном ниже примере мы воспользуемся первым способом, как наиболее наглядным и простым в реализации.

Мы напишем обобщенный класс Lazy<T>, который будет представлять загрузку по требованию, а так же кэшировать результат вычислений для дальнейших обращений к ним.

using System.Linq;
public class Lazy<T> {
private Func<T> func;
private T result;
private bool hasValue;
public Lazy(Func<T> func) {
this.func = func;
this.hasValue = false;
  }
public T Value {
get {
if (!this.hasValue) {
this.result = this.func();
this.hasValue = true;
      }
return this.result;
    }
  }
}

Данный класс имеет три поля:

  • func – делегат Func<T> инкапсулирующего передаваемый метод (находится в пространстве имён Linq, используется для инкапсуляции метода без параметров);
  • result – поле для хранения результата вычислений;
  • hasValue – флаг для обозначния, производились ли уже вычисления.

Как же можно использовать данный класс

Lazy<int> lazy = new Lazy<int>(() => {
    Console.WriteLine("calculating...");
return 42;
  });
Console.WriteLine("starting...");
Console.WriteLine("result = {0}", lazy.Value);
Console.WriteLine("result (again) = {0}", lazy.Value);

Результат работы программы:

starting…

calculating…

result = 42

result (again) = 42

Мы наглядно видим, как в поле func заносится лямба-выражение, результат которого выводится после вызова свойства Value. Причём повторный вызов свойства выводит кэшированные данные.

Далее, думаю стоит написать обертку-помощник для нашего класса, с целью повышения наглядности работы с ним.

public static class Lazy {
public static Lazy<T> New<T>(Func<T> func) {
return new Lazy<T>(func);
  }
}

Будет он выглядеть примерно так. Кстати, примерно также выглядит System.Nullable (один из стандартных классов .NET).

Используя класс Lazy мы можем создать экземпляр нашего типа, вызвав метод Lazy.New вместо написания new Lazy<int> к примеру. Для ещё пущего повышения наглядности будем использовать атрибут var.

int a = 22, b = 20;
var lazy = Lazy.New(() => {
    Console.WriteLine("calculating...");
return new { Mul = a*b, Sum = a+b };
  });
Console.WriteLine("Mul = {0}, Sum = {1}",
  lazy.Value.Mul, lazy.Value.Sum);

Все наши приобритения продемонстрированы Выше в примере, расчитывающий сумму и произведения двух чисел.

Далее хотелось бы рассмотреть пример со значениями аргументов в качестве значений, инициализируемых по требованию, что несомненно очень удобно при использовании такого языка, как C#, в котором инициализация аргументов происходит немедленно.

static Random rnd = new Random();
static void UseRandomArgument(Lazy<int> a0, Lazy<int> a1) {
int res;
if (rnd.Next(2) == 0)
    res = a0.Value;
else
    res = a1.Value;
  Console.WriteLine("result = {0}", res);
}

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

var calc1 = Lazy.New(() => {
    Console.WriteLine("Calculating #1");
return 42;
  });
var calc2 = Lazy.New(() => {
    Console.WriteLine("Calculating #2");
return 44;
  });
UseRandomArgument(calc1, calc2);
UseRandomArgument(calc1, calc2);

Напишем обработчик для данного метода и посмотрим, какие из аргументов будут проинициализированы:

Calculating #1
Result = 42
Result = 42

Повторим запуск приложения:

Calculating #1
Result = 42
Calculating #2
Result = 44

Пример: Список шрифтов с предосмотром.

 

На картинке ниже представлен эскиз нашего будущего приложения. Он содержит выпадающий список с наименованиями шрифтов, а так же область, в которой будет выводится изображение для выбранного шрифта. Как не трудно догадаться при заполнении списка не очень бы хотелось инициализировать все картинки в память компьютера, в связи с чем мы воспользуемся созданным нами классом Lazy<T>.

Наш класс для хранения информации о шрифтах будет следующим:

private class FontInfo {
public Lazy<Bitmap> Preview { get; set; }
public string Name { get; set; }
}

Метод для генерации и отрисовки изображения шрифта:

private void DrawFontPreview(FontFamily f, Bitmap bmp) {
  Rectangle rc = new Rectangle(0, 0, 300, 200);
  StringFormat sf = new StringFormat();
  sf.Alignment = StringAlignment.Center;
  sf.LineAlignment = StringAlignment.Center;
string lipsum = "Lorem ipsum dolor sit amet, consectetuer " +
    "adipiscing elit. Etiam ut mi blandit turpis euismod malesuada. " +
    "Mauris congue pede vitae lectus. Ut faucibus dignissim diam. ";
using (Font fnt = new Font(f, 15, FontStyle.Regular))
using (Graphics gr = Graphics.FromImage(bmp)) {
    gr.FillRectangle(Brushes.White, rc);
    gr.DrawString(lipsum, fnt, Brushes.Black, rc, sf);
  }
}

На загрузке формы нашего приложения связываем наши значения с методом выбора шрифта из списка.

private void FontForm_Load(object sender, EventArgs e) {
  var fontInfo = FontFamily.Families.Select(f => {
// Создаем значение по требованию для картинки
      var preview = Lazy.New(() => {
          Bitmap bmp = new Bitmap(300, 200);
          DrawFontPreview(f, bmp);
return bmp;
        });
// Возвращаем шрифт с названием и превьюшкой
      return new FontInfo { Name = f.Name, Preview = preview };
    });
// Используем дата-байдинг для заполнения списка
  fontCombo.DataSource = fontInfo.ToList();
  fontCombo.DisplayMember = "Name";
}

При изменении выбранного шрифта перерисовываем изображение:

private void fontCombo_SelectedIndexChanged(object sender, EventArgs e) {
  FontInfo fnt = (FontInfo)fontCombo.SelectedItem;
  fontPreview.Image = fnt.Preview.Value;
}

Заключение

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

Материалы

 

  1. http://msdn.microsoft.com/en-us/vcsharp/bb870976.aspx (При написании данной статьи я руководствовался данной работой, все рассмотренные примеры, взяты там же)
  2. Мартин Фаулер “Архитектура корпоративных приложений” (Идея данного паттерна изложена М. Фаулером в этой книге)

5 комментариев on “Lazy Computation in C# (Ленивые вычисления в C#)”

  1. Igor:

    Ваш фид на девелоперс идёт с битыми ссылками — невозможно перейти на ваш блог.

  2. DiavolinoAngelo:

    Прекрасная статья!
    Просто, доходчиво, практично.
    Сразу же нашлась масса применений шаблона в текущих разработках. Особенно после выкуривания идеологии ленивых вычислений и многочисленных примеров на Haskell.
    Огромное спасибо автору!

  3. Oleg:

    А можно так сделать: например, есть массив сложных объектов, и мы хотим, чтобы каждый из объектов загружался по требованию?
    Например
    BigObjects[] b = new BigObjects[10000];

    При этом клиент может запросить b[345] или b[1021], и объекты должны создаваться только когда их запрашивают


Оставьте комментарий