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

391
我找到了一篇关于Lazy<T>的文章:C# 4.0中的延迟加载
有人能指导我在实际应用中如何使用Lazy<T>类吗?换句话说,我应该在什么时候使用它?

51
替换代码:get { if (foo == null) foo = new Foo(); return foo; }。而且有无数个可能使用它的地方... - Kirk Woll
68
请注意,get { if (foo == null) foo = new Foo(); return foo; } 不是线程安全的,而 Lazy<T> 默认是线程安全的。 - Matthew
26
重要提示:懒加载是线程安全的,但它不能在创建对象后保护对象。除非该类型是线程安全的,否则在访问对象之前必须锁定对象。 - Pedro.The.Kid
7个回答

293

当你想要实例化某个东西,且希望在第一次使用时才实例化它时,通常会使用它。这样可以推迟创建它的成本,直到需要它为止,而不总是承担成本。

通常情况下,当对象可能被使用或可能不被使用,并且构造它的成本是相当大的时,使用该方法是最好的选择。


144
为什么不总是使用惰性求值? - TruthOf42
55
首次使用会产生成本,并可能需要一些锁定开销(或者牺牲线程安全性)。因此,应该仔细选择,并且只在必要时使用。 - James Michael Hare
3
James,请问你能详细说明一下“构造成本不可忽略”吗?在我的情况下,我有19个属性,但在大多数情况下只会使用其中2或3个。因此,我考虑使用Lazy<T>实现每个属性。但是,创建每个属性需要进行线性插值(或双线性插值),这相当简单但确实需要一定的成本。(你会建议我自己进行实验吗?) - Ben
3
詹姆斯,我按自己的建议进行了实验。请看我的帖子 - Ben
25
为了防止高吞吐量、低延迟系统中的用户延迟,您可能希望在系统启动期间初始化/实例化所有内容。这只是不“总是”使用“懒加载”的众多原因之一。 - Derrick
显示剩余4条评论

142

您应该尽量避免使用单例模式,但如果确实需要使用,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;
}

48
我讨厌阅读。当我使用单例模式时,你应该尽量避免使用它们 :D ...现在我需要学习为什么应该避免使用它们 :D - Bart Calixto
40
当微软在他们的示例中不再使用单例模式时,我也会停止使用它们。 - eaglei22
14
我倾向于不同意需要避免单例的观点。在遵循依赖注入范式时,这并不重要。理想情况下,所有依赖项都应该只被创建一次。这会减少高负载场景下GC的压力。因此,从类内部将它们设置为单例是可以的。大多数(如果不是全部)现代依赖注入容器都可以处理你选择的任何方式。 - Lee Grissom
2
你不必像那样使用单例模式,而是可以使用任何依赖注入容器将你的类配置为单例。容器会为你处理繁琐的细节。 - VivekDev
4
每样事物都有其目的,使用单例模式的情况有好也有坏 : )。 - Hawkzey
显示剩余3条评论

108
一个非常好的现实世界中需要使用懒加载的例子是ORM(对象关系映射器),比如Entity Framework和NHibernate。

假设你有一个名为Customer的实体,它具有Name、PhoneNumber和Orders属性。其中Name和PhoneNumber是普通字符串,但Orders是一个导航属性,返回客户曾经下过的每个订单的列表。

你通常可能想要浏览所有客户,并获取他们的姓名和电话号码以便联系他们。这是一项非常快速和简单的任务,但是想象一下,如果每次创建客户时,它都会自动进行复杂的连接来返回数千个订单,那将会是多么糟糕。最糟糕的是,你甚至不会使用这些订单,所以这是对资源的完全浪费!

这就是懒加载的完美应用场所,因为如果订单属性是懒加载的,它只会在你真正需要它们时才去获取客户的所有订单。你可以枚举Customer对象仅获取他们的Name和Phone Number,而Order属性则会耐心等待,准备好在你需要它时立即返回。

45
这是一个不好的例子,因为通常懒加载已经内置于对象关系映射(ORM)中。你不应该添加 Lazy<T> 值到你的 POCO 中以实现懒加载,而应该使用 ORM 特定的方法来实现。 - Dynalon
72
@Dyna, 这个例子是在讨论ORM的内置懒加载机制,因为我认为它清晰而简单地展示了懒加载的实用性。 - Despertar
如果您正在使用Entity Framework,应该强制使用自己的延迟加载吗?还是EF会为您执行它? - Zapnologica
8
@Zapnologica EF 默认为您执行所有这些操作。实际上,如果您想要贪婪加载(与延迟加载相反),您必须明确告诉 EF,使用 Db.Customers.Include("Orders")。这将导致在那一刻执行订单连接,而不是首次使用 Customer.Orders 属性时。也可以通过 DbContext 禁用延迟加载。 - Despertar
3
这实际上是个很好的例子,因为在使用类似 Dapper 这样的东西时可能会想要添加这个功能。 - tbone
显示剩余4条评论

49
我一直在考虑使用Lazy<T>属性来帮助提高我的代码性能(并学习更多关于它的知识)。我来这里寻找有关何时使用它的答案,但似乎无论我去哪里都会出现以下短语:

使用延迟初始化来推迟创建大型或资源密集型对象,或执行资源密集型任务,特别是当这种创建或执行可能不会在程序生命周期内发生时。

来自MSDN Lazy<T> Class 我有点困惑,因为我不确定该如何划分界限。例如,我认为线性插值是一个相当快速的计算,但如果我不需要进行插值,那么延迟初始化可以帮助我避免进行插值吗?这是否值得?
最终,我决定尝试自己的测试,并在这里分享结果。不幸的是,我不是做这些测试的专家,所以我很乐意听取建议。 描述 对于我的情况,我特别想看看Lazy属性是否可以帮助改善我的代码的某个部分,该部分进行了大量的插值(其中大部分未使用),因此我创建了一个测试来比较3种方法。
我创建了一个单独的测试类,每个方法都有20个测试属性(让我们称之为t属性)。
  • GetInterp Class:每次获取t属性时运行线性插值。
  • InitInterp Class:通过在构造函数中对每个t属性运行线性插值来初始化t属性。获取操作只返回一个double。
  • InitLazy Class:将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 实例化速度最快,因为它没有执行任何操作。与 InitInterp 相比,InitLazy 实例化速度更快,表明设置延迟属性的开销比我的线性插值计算更快。然而,我有点困惑,因为 InitInterp 应该执行 20 次线性插值(以设置 t 属性),但仅在实例化时占用了 0.09 毫秒(测试1)。相比之下,GetInterp 首次进行一次线性插值需要 0.28 毫秒(测试2),第二次只需 0.1 毫秒(测试3)。

首次获取属性时,InitLazy 的时间几乎是 GetInterp 的两倍长,而 InitInterp 最快,因为它在实例化期间填充了其属性。(至少应该这样做,但为什么实例化结果比单个线性插值快得多?它何时进行这些插值?)

不幸的是,我的测试中存在某些自动代码优化。首次获取属性时,GetInterp 应该与第二次获取属性时所需时间相同,但它的速度显示为快了两倍以上。看起来这种优化也会影响其他类,因为它们在测试3中的速度几乎相同。然而,这样的优化也可能发生在我的生产代码中,这也可能是一个重要的考虑因素。

结论

虽然有些结果如预期,但也有一些非常有趣且出乎意料的结果,可能是由于代码优化引起的。即使对于看起来在构造函数中执行了很多工作的类,实例化结果也表明它们可能非常快速地创建,相比之下获取 double 属性则慢得多。虽然领域专家可能能够进行更深入的评论和调查,但我个人感觉需要在我的生产代码上再次进行此测试,以研究可能正在发生的优化类型。然而,我期望 InitInterp 可能是最佳选择。


35
也许你应该发布用于重现输出的测试代码,因为如果不了解你的代码,很难提出建议。 - WiiMaxx
2
我认为主要的权衡是在内存使用(惰性)和 CPU 使用(非惰性)之间。因为“惰性”需要做一些额外的簿记,所以“InitLazy”将比其他解决方案使用更多的内存。它也可能会在每次访问时产生轻微的性能损失,同时检查它是否已经有一个值;聪明的技巧可以消除这种开销,但它需要 IL 中的特殊支持。(Haskell 通过使每个惰性值成为函数调用来实现这一点;一旦生成了该值,它就被替换为返回该值的函数。) - jpaugh

22
只是为了扩展一下Matthew发布的例子:
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; }
    }
}

在Lazy成为框架的一部分之前,我们会这样做:
private static object lockingObject = new object();
public static LazySample InstanceCreation()
{
    if (lazilyInitObject == null)
    {
         lock (lockingObject)
         {
              if (lazilyInitObject == null)
              {
                   lazilyInitObject = new LazySample();
              }
         }
    }
    return lazilyInitObject;
}

7
我通常使用一个IoC容器来实现这个。 - Jowen
1
我非常赞同考虑使用IoC容器。然而,如果您想要一个简单的延迟初始化的对象单例,请考虑一下,如果您不需要这个线程安全,手动使用If可能是最好的选择,考虑到Lazy处理自身的性能开销。 - Thulani Chivandikwa

15

来自MSDN:

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

除了James Michael Hare的答案外,Lazy提供线程安全初始化您的值。请参阅LazyThreadSafetyMode枚举MSDN条目,描述此类的各种类型的线程安全模式。


-5
你应该看一下这个例子,以了解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);"

输出 --> 值未创建


这个例子很糟糕。创建一个空的List<T>是一个非常快速的操作,而且List<T>不是线程安全的。在任何情况下,延迟初始化一个空的List<T>都是没有意义的。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接