什么时候应该使用Lazy <T>?


327

我发现这篇文章是关于 LazyC#4.0中的惰性-惰性

使用惰性对象具有最佳性能的最佳实践是什么?有人可以指出我在实际应用中的实际用途吗?换句话说,什么时候应该使用它?


42
替换为:get { if (foo == null) foo = new Foo(); return foo; }。并且有无数的可能使用它的地方……
柯克·沃尔

57
请注意,get { if (foo == null) foo = new Foo(); return foo; }它不是线程安全的,而Lazy<T>默认情况下是线程安全的。
马修(Matthew)

23
来自MSDN:重要信息:惰性初始化是线程安全的,但创建后不会保护对象。除非类型是线程安全的,否则必须在访问对象之前将其锁定。
Pedro.The.Kid 2014年

Answers:


237

通常,当您想在第一次实际使用某个实例时实例化它。这将创建它的成本延迟到需要时/而不是始终产生成本。

通常,当可以使用或不使用该对象并且构造它的成本很重要时,这是优选的。


121
为什么不总是使用Lazy?
TruthOf42 2013年

44
这样做会招致初次使用的费用,并且可能会花费一些锁定开销(或者如果没有则牺牲线程安全性)。因此,应该仔细选择它,除非需要,否则不要使用它。
詹姆斯·迈克尔·黑尔

3
詹姆斯,请您谈谈“而且建造成本是不平凡的”吗?就我而言,我班上有19个属性,在大多数情况下,只需要查看2或3个属性。因此,我正在考虑使用实现每个属性Lazy<T>。但是,要创建每个属性,我需要进行线性内插(或双线性内插),这很简单,但确实需要一定的成本。(你会建议我去,做自己的实验?)

3
詹姆斯,根据我的建议,我做了自己的实验。看我的帖子
2014年

17
您可能需要初始化/实例化系统“启动”期间的所有内容,以防止高吞吐量,低延迟的系统中的用户延迟。这只是不“总是”使用Lazy的众多原因之一。
Derrick

126

您应该尝试避免使用Singleton,但是如果需要,Lazy<T>可以轻松实现懒惰的,线程安全的Singleton:

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
Bart Calixto 2013年

24
当Microsoft在示例中停止使用Singletons时,我将停止使用它们。
eaglei22

4
我倾向于不同意需要避免使用Singletons的观点。当遵循依赖注入范例时,无论哪种方式都没有关系。理想情况下,所有依赖项仅应创建一次。在高负载情况下,这可以降低GC的压力。因此,从类本身内部使它们成为Singleton很好。大多数(如果不是全部)现代DI容器都可以按照您选择的任何一种方式进行处理。
Lee Grissom

1
您不必使用这样的单例模式,而可以使用任何di容器为单例配置您的类。容器将为您处理开销。
VivekDev

一切都有目的,在某些情况下单例是一种很好的方法,而在某些情况下则不是:)。
Hawkzey

86

延迟加载非常有用的一个现实世界的好例子是使用ORM(对象关系映射器),例如Entity Framework和NHibernate。

假设您有一个实体客户,该客户具有名称,电话号码和订单的属性。Name和PhoneNumber是常规字符串,但是Orders是一个导航属性,它返回客户曾经做过的每个订单的列表。

您通常可能需要遍历所有客户,并获取他们的姓名和电话号码以致电给他们。这是一个非常快速和简单的任务,但是想象一下,如果您每次创建一个客户,它都会自动进行并执行复杂的联接以返回数千个订单。最糟糕的是,您甚至都不会使用这些订单,因此完全浪费了资源!

这是延迟加载的理想场所,因为如果Order属性是延迟的,除非您确实需要,否则它将不会获取所有客户的订单。您可以枚举在Order属性耐心睡眠时,Customer对象仅获得其名称和电话号码,以备您需要时使用。


34
不好的例子,因为这种延迟加载通常已经内置在ORM中。您不应该开始将Lazy <T>值添加到POCO来进行延迟加载,而应使用特定于ORM的方法来执行此操作。
Dynalon

56
@Dyna此示例是指ORM的内置延迟加载,因为我认为这以一种清晰,简单的方式例证了延迟加载的有用性。
Despertar

因此,如果您使用的是Entity Framework,是否应该强制自己实施懒惰?还是EF为您做到了?
Zapnologica

7
@Zapnologica EF默认情况下会为您完成所有这些操作。实际上,如果您希望进行快速加载(与延迟加载相反),则必须使用显式告知EF Db.Customers.Include("Orders")。这将导致订单联接在那一刻而不是在Customer.Orders首次使用该属性时执行。也可以通过DbContext禁用延迟加载。
Despertar 2014年

2
实际上,这是一个很好的例子,因为在使用Dapper之类的程序时可能要添加此功能。
tbone

41

我一直在考虑使用Lazy<T>属性来帮助改善我自己的代码的性能(并进一步了解它)。我来这里是为了寻找有关何时使用它的答案,但是似乎我走到哪里都有类似的短语:

使用惰性初始化可以延迟大型对象或资源密集型对象的创建或资源密集型任务的执行,尤其是在程序生命周期中可能不会发生此类创建或执行时。

MSDN Lazy <T>类

我有点困惑,因为我不确定在哪里划界线。例如,我认为线性插值是一种相当快速的计算,但是如果我不需要这样做,那么惰性初始化可以帮助我避免这样做,这值得吗?

最后,我决定尝试自己的测试,并认为我会在这里分享结果。不幸的是,我并不是真正从事此类测试的专家,因此,我很高兴收到提出改进建议的意见。

描述

对于我的情况,我特别感兴趣的是看看Lazy Properties是否可以帮助改进代码中进行大量插值的部分(大部分未使用),因此我创建了一个比较3种方法的测试。

我为每种方法创建了一个单独的测试类,该类具有20个测试属性(称为t-properties)。

  • GetInterp类:每次获得t属性时都运行线性插值。
  • InitInterp类:通过对构造函数中的每个插值运行线性插值来初始化t属性。get只会返回一个双精度值。
  • InitLazy类:将t属性设置为Lazy属性,以便在首次获得该属性时运行一次线性插值。后续获取应该只返回已经计算出的double。

测试结果以毫秒为单位,是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属性),但是实例化(测试1)仅花费0.09毫秒,而GetInterp仅执行一次线性插值则花费0​​.28毫秒第一次(测试2),第二次(测试3)需要0.1毫秒。

InitLazyGetInterp第一次获得属性要花几乎两倍的时间,而这InitInterp是最快的,因为它在实例化期间填充了属性。(至少那是应该做的,但是为什么它的实例化结果比单个线性插值要快得多?什么时候才进行这些插值?)

不幸的是,我的测试中似乎正在进行一些自动代码优化。应该要GetInterp同样的时间去为它的第二次首次属性,但它显示为2倍以上更快。看起来这种优化也影响了其他类,因为它们都花费了相同的时间进行测试3。但是,这种优化也可能发生在我自己的生产代码中,这可能也是一个重要的考虑因素。

结论

尽管某些结果是预期的,但也可能由于代码优化而产生了一些非常有趣的意外结果。即使对于看起来像在构造函数中完成大量工作的类,实例化结果也表明,与获得double属性相比,它们的创建速度仍然非常快。尽管该领域的专家可以发表评论并进行更彻底的研究,但我个人的感觉是,我需要对生产代码再次进行此测试,以便检查那里可能还会进行哪种优化。但是,我希望这InitInterp可能是要走的路。


26
也许您应该发布测试代码以重现输出,因为在不知道您的代码的情况下,很难提出任何建议
WiiMaxx 2014年

1
我认为主要的折衷是在内存使用率(惰性)和cpu使用率(非惰性)之间。由于lazy必须做一些额外的记账工作,因此InitLazy会比其他解决方案使用更多的内存。在检查它是否已经有值的同时,它对每次访问的性能可能也会有轻微的影响。巧妙的技巧可以消除这些开销,但是需要IL中的特殊支持。(Haskell通过使每个惰性值成为函数调用来执行此操作;一旦生成该值,它将替换为每次都会返回该值的函数。)
jpaugh

14

只是指出Mathew发布的示例

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容器。
Jowen

1
我强烈同意考虑为此使用IoC容器。但是,如果您想要一个简单的延迟初始化对象单例,则还应考虑一下,如果您不需要这样做是线程安全的,则最好使用If进行手动操作,考虑到延迟处理自身的性能开销。
Thulani Chivandikwa 2015年

12

从MSDN:

使用Lazy实例可以延迟大型对象或资源密集型对象的创建或资源密集型任务的执行,尤其是在程序生命周期中可能不会发生此类创建或执行时。

除了James Michael Hare的答案之外,Lazy还提供了线程安全的值初始化。看一下LazyThreadSafetyMode枚举MSDN条目,该条目描述此类的各种类型的线程安全模式。


-2

您应该看一下这个例子,以了解懒加载架构

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);”

输出->未创建值

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.