何时使用懒加载?

9

我将所有成员进行懒加载。我已经这样做了一段时间,并且仅仅是从表面上认为懒加载是一件好事。

假设我们有:

public class SomeClass
{
   public int anInt;
   public SomeReferenceType member1;

   public SomeClass()
   {
      //initialize members in constructor when needed (lazy load)
      anInt = new int();
      member1 = new SomeReferenceType();
   }
}

这种方式有没有什么缺点?这是一个正确的惰性加载模式吗?对于懒加载值类型 (使用现代 RAM,是否重要) 是否有意义?


从您的回答中我学到了些什么,我想知道上述方法和这个方法之间是否有任何区别...

public class SomeClass
    {
       public int anInt;
       public SomeReferenceType member1 = new SomeReferenceType();

       public SomeClass()
       {

       }
    }

2
你的代码示例正在进行主动加载(eager loading)。 - CodesInChaos
1
我认为在构造函数中初始化不是惰性加载。 - SquidScareMe
1
个人而言,我只在加载非常昂贵且很可能永远不会使用该属性的情况下才使用延迟加载。 - CodesInChaos
1
懒加载是在需要时加载,而不是创建对象时加载。没有必要使用 new 创建一个整数。 - martinstoeckli
我会感到惊讶,至少在那些已经使用 [so] 一段时间的用户中间,如果这是真的话。我想这可能是那些不知道标签用途的人才会这样做。无论如何,他们已经开始删除明显的情况,比如 "[C#] Title",我认为你的风格可能也快要被删除了。 - John Saunders
显示剩余4条评论
9个回答

18

首先,构造函数中初始化成员不是惰性加载。

惰性加载是在第一次请求时初始化成员。在.NET中,以下是一个简单的示例(使用双重检查锁定以避免线程问题):

public class SomeClass
{
    private object _lockObj = new object();
    private SomeReferenceType _someProperty;

    public SomeReferenceType SomeProperty
    {
        get
        {
            if(_someProperty== null)
            {
                lock(_lockObj)
                {
                    if(_someProperty== null)
                    {
                        _someProperty= new SomeReferenceType();
                    }
                }
            }
            return _someProperty;
        }
        set { _someProperty = value; }
    }
}

幸运的是,如果您使用的是.NET 4,现在可以使用Lazy<T>来处理这些问题,使事情变得更加容易。

其次,在您有许多可能成本高昂并且确定会使用所有这些值的成员时,延迟加载是一个不错的选择。该成本会导致类型不必要地变慢实例化。

仅为了懒加载而懒加载只会给您的代码增加不必要的复杂性,并且如果不正确地执行(例如在处理线程时),可能会在以后引发问题。


2
懒加载常被忽视的一个普遍副作用(取决于上下文)是它对用户体验的影响。有时,当您等待直到用户需要某些内容才加载时,它会对应用程序的响应速度产生负面影响。在做出此类决策时,性能分析指标是非常宝贵的。 - Jacob Jennings

8

这并不是真正的懒加载。这是在构造时初始化。通常,我们所说的懒加载是指在第一次引用时构造该项。

    private string _someField;

    public string SomeField
    {
        get 
        {
            // we'd also want to do synchronization if multi-threading.
            if (_someField == null)
            {
                _someField = new String('-', 1000000);
            }

            return _someField;
        }
    }

以前惰性加载的典型方式之一是进行“检查,锁定,再次检查”操作,这样如果已经创建了对象,则不会进行锁定操作。但是由于有可能出现两个项目通过检查并等待锁定的情况,因此需要在锁定操作中再次进行检查。

public class SomeClass
{
    private string _someField;

    private readonly object _lazyLock = new object();


    public string SomeField
    {
        get 
        {
            // we'd also want to do synchronization if multi-threading.
            if (_someField == null)
            {
                lock (_lazyLock)
                {
                    if (_someField == null)
                    {
                        _someField = new String('-', 1000000);
                    }
                }
            }

            return _someField;
        }
    }
}

有许多种方法可以实现此目的,事实上在.NET 4.0中,有一种名为Lazy<T>的类型可以帮助您轻松地实现线程安全的延迟加载。

public class SomeClass
{
    private readonly Lazy<string> _someField = new Lazy<string>(() => new string('-', 10000000), true);

    private readonly object _lazyLock = new object();


    public string SomeField
    {
        get
        {
            return _someField.Value;
        }
    }
}

通常情况下,如果你创建的对象很昂贵(占用内存或时间较多),但不能确定是否会使用它,那么懒加载是一个不错的方案。如果你相当确定它总是会被使用,那么就应该直接构建它。


你应该提到,你的构造仅在 SomeField(它是一个属性,而不是一个字段 :-p)实际上从未为 null 时才有效。很容易想象一种情况,即重计算的结果可能真正为 null,而您希望能够存储该值。 - Timwi
@Timwi:噢,当然,我只是举了一个简单的例子。对于某些情况来说,null是可以接受的值。但通常在这种情况下,没有太多需要进行延迟加载的必要。 - James Michael Hare

6

从我看到的代码中,您没有使用惰性加载。您正在构造函数中初始化成员,这总是会发生,并且在实例的生命周期非常早期就会发生。

因此,我想知道,对于您来说,什么是非惰性加载?

通常情况下,惰性加载是指仅在访问时才初始化某些内容。

以下是一个示例,使用 .NET 4.0 的 Lazy 类,它可以帮助您进行惰性加载:

public class Foo
{
    private Lazy<int> _value = new Lazy<int>(() => 3);

    public int Value { get { return _value.Value; } }
}

关于线程安全性 - 你可以传递第二个参数 LazyThreadSafetyMode,它知道两种指定线程安全性的方法:一种是初始化方法可能会执行多次,但所有的线程都获得先前创建的值,另一种是在保护执行不被多次运行的同时也保证了线程安全性。


我知道我需要在构造函数中初始化引用类型,否则会出现空引用异常。你是说我不需要显式地初始化值类型吗?它们在构造函数调用时会自动初始化吗?如果是的话,这个操作有一个术语吗? - P.Brian.Mackey
这只是初始化。默认初始化会将所有实例字段设置为其默认值(引用类型为null,值类型为默认值)。延迟加载明确意味着在最后可能的时刻实例化某个东西,即在访问资源时。 - flq

1

有/没有惰性加载的示例

考虑一个作者可能有零到多本书

没有惰性加载:

public class Author
{
public int Id {get;set;}
public string Name  {get;set;}
public Icollecation<Book> Books {get;set;}

}

public class Book
{
public int Id {get;set;}
public string Title{get;set;}
public Author Author {get;set;}
public int yearOfrelease {get;set;}

}

// Get the info from books, consider my view has book name, title, author name
await _context.Books.ToListAsync();

这将带来以下结果:

书名 标题 作者 年份
书籍1 标题1 2022
书籍2 标题2 2021

我们可以看到作者一栏是空的。要加载作者,我需要添加include()。

await _context.Books.include(a=>a.Author).ToListAsync();

现在我得到了以下内容:
书名 标题 作者 年份
书籍1 标题1 作者1 2022
书籍2 标题2 作者2 2021

这就是我们所说的急切加载。

使用延迟加载,我必须将相关表添加为虚拟表。

public class Author
{
public int Id {get;set;}
public string Name  {get;set;}
public virtual Icollecation<Book> Books {get;set;}

}

public class Book
{
public int Id {get;set;}
public string Title{get;set;}
public virtual Author Author {get;set;}
public int yearOfrelease {get;set;}

}

现在只需调用这些书籍,您将获得所有相关数据。

await _context.Books.ToListAsync();
书名 标题 作者 年份
书籍1 标题1 作者1 2022年
书籍2 标题2 作者2 2021年

换句话说,在惰性加载中,我们将对象的加载延迟到需要它的时候。因此,使用惰性加载,我们会加载所有书籍信息,无论您是否要使用它们。


0

这不是懒加载。

懒加载意味着只有在真正访问时才加载值(这在初始化程序中不会发生)。

懒加载类似于:

private SomeClass _someRef = null;
public SomeClass SomeRef
{
  get
  {
    if(_someRef == null)
    {
       //initialisation just in case of access
       _someRef = new  SomeClass();
    }
    return _someRef;
  }
}

你应该提到,你的构造仅在 SomeRef 实际上不为 null 时才有效。很容易想象出一种情况,即重计算的结果可能真正为 null,而你希望能够存储它。 - Timwi

0
一个真正的延迟加载属性,对于一个整数可能看起来像这样:
private int? _heavyLoadedInt;

public int HeavyLoading
{
    get
    {
        if (_heavyLoadedInt == null)
            _heavyLoadedInt = DoHeavyLoading();
        return _heavyLoadedInt.Value;
    }
}

现在,如果你看一下这个代码,你会发现这里有一些额外的开销:你必须将值存储在可空类型中(额外的内存);每次访问时都要检查它是否为null,并且每次访问时都要从可空类型中检索值。

如果你的整数确实需要进行一些非常重的计算,那么这种构造是有意义的。但是new int()不是一个重量级的计算,它只返回0。这种开销很小,但如果你将这种开销加到一个更小的操作上(即读取一个整数),那就没有意义了。


在这里,您还必须提到可空类型仅适用于值类型...因此,对于值类型的重型计算,您的构造是有意义的,但不适用于昂贵(大)的引用类型。 - fixagon

0
List<int> number = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        var result = number.Where(x => x % 2 == 0);
        number.Add(20);
        //foreach (int item in result)
        //{
        //    Console.WriteLine("loop1:" + item);
        //}

        foreach (int item in result)
        {
            if (item == 4)
                break;
            Console.WriteLine("loop2:" + item);
        }
        number.Add(40);
        foreach (int item in result)
        {

            Console.WriteLine("loop3:"+item);
        }
        Console.ReadLine();

取消第一个循环的注释并查看差异。非常使用示例来理解延迟执行和惰性加载。


0

懒加载是一种概念,我们将对象的加载延迟到需要它的时候。简单来说,按需对象加载而不是不必要地加载对象。

例如,考虑下面的示例,其中我们有一个简单的Customer类,这个Customer类内部有许多Order对象。仔细看一下Customer类的构造函数。当创建Customer对象时,它也会在那一刻加载Order对象。因此,即使我们需要或不需要Order对象,它仍然被加载。

示例链接


请适当添加代码,而非图片。 - Maelig

0

当对象创建的成本非常高且对象的使用非常罕见时,懒加载是必不可少的。因此,在这种情况下实现懒加载是值得的。 懒加载的基本思想是在需要时加载对象/数据。


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