Когда я должен использовать Lazy <T>?

327

Я нашел эту статью о Lazy: лень в C # 4.0 - ленивый

Какова лучшая практика, чтобы иметь лучшую производительность, используя ленивые объекты? Может ли кто-нибудь указать мне на практическое использование в реальном приложении? Другими словами, когда я должен использовать это?

danyolgiax
источник
42
Заменяет: get { if (foo == null) foo = new Foo(); return foo; }. И есть миллионы возможных мест, чтобы использовать это ...
Кирк Уолл
57
Обратите внимание, что get { if (foo == null) foo = new Foo(); return foo; }не является потокобезопасным, хотя Lazy<T>по умолчанию является потокобезопасным.
Мэтью
23
Из MSDN: ВАЖНО: Ленивая инициализация поточно-ориентирована, но не защищает объект после создания. Вы должны заблокировать объект перед тем, как получить к нему доступ, если тип не является потокобезопасным.
Pedro.The.Kid

Ответы:

237

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

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

Джеймс Майкл Хэйр
источник
121
почему бы не ВСЕГДА использовать Lazy?
TruthOf42
44
Это влечет за собой затраты при первом использовании и может использовать некоторые накладные расходы на блокировку (или жертвовать безопасностью потока, если нет). Таким образом, его следует выбирать осторожно и не использовать без необходимости.
Джеймс Майкл Хэйр
3
Джеймс, не могли бы вы рассказать подробнее "а стоимость строительства нетривиальна"? В моем случае у меня есть 19 свойств в моем классе, и в большинстве случаев только 2 или 3 когда-либо нужно будет рассмотреть. Поэтому я рассматриваю реализацию каждого свойства, используя Lazy<T>. Однако для создания каждого свойства я делаю линейную интерполяцию (или билинейную интерполяцию), которая довольно тривиальна, но имеет определенную стоимость. (Собираетесь ли вы предложить мне пойти и провести собственный эксперимент?)
Бен
3
Джеймс, следуя моему собственному совету, я провел свой собственный эксперимент. Смотрите мой пост .
Бен
17
Возможно, вы захотите инициализировать / создать все элементы при запуске системы, чтобы предотвратить задержку пользователя в системах с высокой пропускной способностью и низкой задержкой. Это только одна из многих причин не всегда использовать Lazy.
Деррик
126

Вы должны стараться избегать использования Singletons, но, если вам это когда-нибудь понадобится, Lazy<T>облегчает реализацию отложенных, поточно-ориентированных синглетонов:

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    Singleton()
    {
        // Explicit private constructor to prevent default public constructor.
        ...
    }

    public static Singleton Instance => instanceHolder.Value;
}
Мэтью
источник
38
Я ненавижу читать. Вы должны стараться избегать использования синглетонов, когда я их использую: D ... теперь мне нужно узнать, почему я должен стараться избегать их: D
Барт Каликсто
24
Я перестану использовать Singletons, когда Microsoft перестанет использовать их в своих примерах.
eaglei22
4
Я склонен не соглашаться с понятием необходимости избегать синглетонов. Следуя парадигме внедрения зависимостей, это не должно иметь значения в любом случае. В идеале все ваши зависимости должны создаваться только один раз. Это снижает нагрузку на ГХ в сценариях с высокой нагрузкой. Поэтому, делать их синглтоном из самого класса - это хорошо. Большинство (если не все) современных DI-контейнеров могут обрабатывать их так, как вы выберете.
Ли Гриссом
1
Вам не нужно использовать подобный шаблон синглтона, вместо этого используйте любой контейнер di, сконфигурирующий ваш класс для синглтона. Контейнер позаботится о накладных расходах для вас.
VivekDev
У всего есть цель, есть ситуации, когда синглтоны - это хороший подход, и ситуации, когда это не так :).
Хокзи
86

Отличный реальный пример того, как полезна ленивая загрузка, - это ORM (Object Relation Mappers), такие как Entity Framework и NHibernate.

Допустим, у вас есть объект Customer, у которого есть свойства для Name, PhoneNumber и Orders. Name и PhoneNumber являются обычными строками, но Orders - это свойство навигации, которое возвращает список всех заказов, которые когда-либо делал клиент.

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

Это идеальное место для ленивой загрузки, потому что если свойство Order является ленивым, оно не пойдет за все заказы клиента, если они вам действительно не нужны. Вы можете перечислить объекты Customer, получая только их Имя и Номер телефона, пока свойство Order терпеливо спит и готово, когда вам это нужно.

Деспертар
источник
34
Плохой пример, поскольку такая ленивая загрузка обычно уже встроена в ORM. Вы не должны начинать добавлять значения Lazy <T> в свои POCO для получения отложенной загрузки, но используйте для этого специфический для ORM способ.
Диналон
56
@Dyna Этот пример относится к встроенной отложенной загрузке ORM, потому что я думаю, что это иллюстрирует полезность отложенной загрузки простым и понятным способом.
Despertar
Так что, если вы используете Entity Framework, нужно ли применять свои собственные ленивые? Или EF делает это для вас?
Zapnologica
7
@Zapnologica EF делает все это для вас по умолчанию. На самом деле, если вы хотите загружаться (в противоположность отложенной загрузке), вы должны явно указать EF с помощью Db.Customers.Include("Orders"). Это приведет к тому, что объединение заказов будет выполнено в тот момент, а не при Customer.Ordersпервом использовании свойства. Ленивая загрузка также может быть отключена через DbContext.
Despertar
2
На самом деле это хороший пример, так как можно добавить эту функциональность при использовании чего-то вроде Dapper.
TBone
41

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

Используйте отложенную инициализацию, чтобы отложить создание большого или ресурсоемкого объекта или выполнение ресурсоемкой задачи, особенно когда такое создание или выполнение может не произойти в течение срока жизни программы.

из MSDN Lazy <T> класса

Я немного растерялся, потому что не уверен, где провести черту. Например, я рассматриваю линейную интерполяцию как довольно быстрое вычисление, но если мне не нужно это делать, то может ли ленивая инициализация помочь мне избежать этого и стоит ли это того?

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

Описание

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

Я создал отдельный тестовый класс с 20 тестовыми свойствами (давайте назовем их t-свойствами) для каждого подхода.

  • GetInterp Class: Запускает линейную интерполяцию каждый раз, когда получается t-свойство.
  • Класс InitInterp: Инициализирует t-свойства, выполняя линейную интерполяцию для каждого в конструкторе. Get просто возвращает двойной.
  • Класс InitLazy: задает t-свойства как свойства Lazy, чтобы линейная интерполяция запускалась один раз при получении свойства в первый раз. Последующее получение должно просто вернуть уже рассчитанный дубль.

Результаты теста измеряются в мс и представляют собой среднее значение 50 экземпляров или 20 полученных значений. Затем каждый тест был выполнен 5 раз.

Результаты теста 1: Реализация (в среднем 50 экземпляров)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.005668 0.005722 0.006704 0.006652 0.005572 0.0060636 6.72
InitInterp 0.08481  0.084908 0.099328 0.098626 0.083774 0.0902892 100.00
InitLazy   0.058436 0.05891  0.068046 0.068108 0.060648 0.0628296 69.59

Результаты теста 2: Первый получить (в среднем 20 объектов получает)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.263    0.268725 0.31373  0.263745 0.279675 0.277775 54.38
InitInterp 0.16316  0.161845 0.18675  0.163535 0.173625 0.169783 33.24
InitLazy   0.46932  0.55299  0.54726  0.47878  0.505635 0.510797 100.00

Результаты теста 3: второе получение (в среднем 20 приобретений недвижимости)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.08184  0.129325 0.112035 0.097575 0.098695 0.103894 85.30
InitInterp 0.102755 0.128865 0.111335 0.10137  0.106045 0.110074 90.37
InitLazy   0.19603  0.105715 0.107975 0.10034  0.098935 0.121799 100.00

наблюдения

GetInterpбыстрее всего создать экземпляр, как и ожидалось, потому что он ничего не делает. InitLazyбыстрее создать экземпляр, чем InitInterpпредполагать, что накладные расходы при настройке отложенных свойств быстрее, чем мои вычисления с линейной интерполяцией. Тем не менее, я немного запутался, потому что InitInterpдолжен выполнить 20 линейных интерполяций (чтобы установить его t-свойства), но для его создания требуется всего 0,09 мс (тест 1), по сравнению сGetInterp 0,28 мс, чтобы выполнить только одну линейную интерполяцию в первый раз (тест 2) и 0,1 мс, чтобы сделать это во второй раз (тест 3).

Требуется InitLazyпочти в 2 раза больше времени, чем GetInterpдля получения свойства в первый раз, и при этом InitInterpявляется самым быстрым, потому что он заполняет свои свойства во время создания экземпляра. (По крайней мере, это то, что он должен был сделать, но почему его результат был гораздо быстрее, чем одна линейная интерполяция? Когда именно он выполняет эти интерполяции?)

К сожалению, похоже, что в моих тестах происходит некоторая автоматическая оптимизация кода. Он должен принять GetInterpто же самое время , чтобы получить собственность в первый раз , как это делает во второй раз, но он показывает , как более чем в 2 раза быстрее. Похоже, что эта оптимизация также влияет на другие классы, так как все они занимают примерно одинаковое количество времени для теста 3. Однако такая оптимизация может также иметь место в моем собственном рабочем коде, что также может быть важным фактором.

Выводы

Хотя некоторые результаты ожидаемые, есть и некоторые очень интересные неожиданные результаты, вероятно, из-за оптимизации кода. Даже для классов, которые выглядят так, как будто они выполняют большую часть работы в конструкторе, результаты создания экземпляров показывают, что они все еще могут быть созданы очень быстро по сравнению с получением двойного свойства. Хотя специалисты в этой области могут комментировать и исследовать более тщательно, я лично считаю, что мне нужно повторить этот тест снова, но с моим рабочим кодом, чтобы выяснить, какая оптимизация может происходить там же. Тем не менее, я ожидаю, что это InitInterpможет быть путь.

Бен
источник
26
возможно, вам следует опубликовать свой тестовый код для воспроизведения выходных данных, потому что, не зная вашего кода, будет сложно что-либо предложить
WiiMaxx
1
Я считаю, что основной компромисс между использованием памяти (ленивый) и использованием процессора (не ленивый). Потому lazyчто нужно сделать некоторые дополнительные бухгалтерии, InitLazyбудет использовать больше памяти, чем другие решения. Он также может иметь незначительное снижение производительности при каждом доступе, в то время как он проверяет, имеет ли он уже значение или нет; умные трюки могут убрать эти издержки, но для этого потребуется специальная поддержка в IL. (Haskell делает это, превращая каждое ленивое значение в вызов функции; после того, как значение сгенерировано, оно заменяется функцией, которая каждый раз возвращает это значение.)
jpaugh
14

Просто чтобы указать на пример, опубликованный Мэтью

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    private static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    private Singleton()
    {
        ...
    }

    public static Singleton Instance
    {
        get { return instanceHolder.Value; }
    }
}

до рождения Ленивого мы бы сделали это так:

private static object lockingObject = new object();
public static LazySample InstanceCreation()
{
    if(lazilyInitObject == null)
    {
         lock (lockingObject)
         {
              if(lazilyInitObject == null)
              {
                   lazilyInitObject = new LazySample ();
              }
         }
    }
    return lazilyInitObject ;
}
Тулани Чивандиква
источник
6
Я всегда использую контейнер IoC для этого.
Йовен
1
Я полностью согласен на рассмотрение контейнера IoC для этого. Однако, если вам нужен простой ленивый инициализированный объект-одиночка, также учтите, что если вам не нужно, чтобы это было безопасным для работы с потоками, делать это вручную с помощью If может быть лучше, если учесть издержки, связанные с тем, как Lazy обрабатывает себя.
Тулани Чивандиква
12

Из MSDN:

Используйте экземпляр Lazy, чтобы отложить создание большого или ресурсоемкого объекта или выполнение ресурсоемкой задачи, особенно когда такое создание или выполнение может не произойти в течение срока жизни программы.

В дополнение к ответу Джеймса Майкла Хэра, Lazy обеспечивает поточно-ориентированную инициализацию вашего значения. Взгляните на запись MSDN перечисления LazyThreadSafetyMode, описывающую различные типы режимов безопасности потоков для этого класса.

Vasea
источник
-2

Вы должны посмотреть этот пример, чтобы понять архитектуру Lazy Loading

private readonly Lazy<List<int>> list = new Lazy<List<int>>(() =>
{
    List<int> configList = new List<int>(Thread.CurrentThread.ManagedThreadId);
    return configList;
});
public void Execute()
{
    list.Value.Add(0);
    if (list.IsValueCreated)
    {
        list.Value.Add(1);
        list.Value.Add(2);

        foreach (var item in list.Value)
        {
            Console.WriteLine(item);
        }
    }
    else
    {
        Console.WriteLine("Value not created");
    }
}

-> вывод -> 0 1 2

но если этот код не напишите "list.Value.Add (0);"

вывод -> значение не создано

Туфья утка
источник