Lazy<T> 的缺点是什么?

49

我最近开始在我的应用程序中广泛使用Lazy,我想知道在使用Lazy<T>时是否有任何明显的负面影响需要考虑?

我试图尽可能多地利用Lazy<T>,主要是为了帮助减少我们已加载但未激活插件的内存占用。


4
我刚开始使用 Lazy<T>,发现它通常意味着设计不良或程序员的懒惰。此外,一个缺点是你需要更加警惕作用域变量,并创建适当的闭包。 - Gleno
4
@Gleno 为什么这位程序员会被说成是懒惰的? - Anton Gogolev
4
@Gleno,Anton:更重要的是,为什么它是不好的?我总是在我的编程课上教导学生,懒惰是程序员中的一种重要美德。 - Konrad Rudolph
2
我也赞成懒惰,但有时候进行惰性求值可能比仔细思考将使用哪个确切的资源更容易。在这种情况下,你可能会错过理解、简化和美化自己代码的机会。 - Gleno
7个回答

19

我想进一步解释我的评论,它写道:

我刚开始使用 Lazy,发现它经常表明设计存在问题;或者是程序员的懒惰。另外,一个缺点是你必须更加警惕作用域变量,并创建适当的闭包。

例如,我使用 Lazy<T> 来创建我的(无会话)MVC应用中用户可以看到的页面。这是一个引导式向导,因此用户可能要前往随机的上一个步骤。当握手完成后,创建了一个Lazy<Page>对象数组,如果用户指定了一个步骤,那么就会评估该精确页面。 我发现它提供了良好的性能,但其中有一些方面我不喜欢,例如许多我的foreach结构现在看起来像这样:

foreach(var something in somethings){
     var somethingClosure = something;
     list.Add(new Lazy<Page>(() => new Page(somethingClosure));
} 

也就是说,你必须非常主动地处理闭包的问题。否则,我认为将一个lambda表达式存储起来,在需要时再进行评估并不会带来太大的性能损失。

另一方面,这也可能表明程序员正在成为一个“懒惰的Programmer”,也就是说,你更愿意不去思考程序,而是让正确的逻辑在需要时进行评估,就像在我的例子中一样——我可以只找出所需页面的具体内容,而不是构建整个数组;但我选择了懒惰的方式,采取了全盘接受的方法。

编辑

我意识到,在处理并发时,Lazy<T>也有一些奇特之处。例如,在某些情况下可以使用ThreadLocal<T>,还有一些标志配置可供您的特定多线程场景使用。你可以在MSDN上阅读更多相关信息。


3
这并不是 Lazy<T> 本身的问题,而是你使用它的方式。 - Anton Gogolev
3
@Anton,是的;我的推测是,Lazy<>在给你不寻求更好解决方案的选择时有时会存在问题。如果有这个选项,你可能会满足于能够工作的东西。 - Gleno
@Fuji,这样说吧——最糟糕的情况就是由于规格变更,你突然不得不评估所有的Lazy对象,或者面临一次重大重写。你能接受这种情况吗? - Gleno
1
+1 对于 var somethingClosure = something; 我一直在寻找一个称呼我的闭包... 哦,我真是后悔莫及。 - satnhak
1
请注意,在C# 5.0(VS2012)中,默认情况下在闭包中使用当前迭代值...您不再需要处理令人讨厌的“额外本地变量闭包”问题。有关详细信息,请参见http://msdn.microsoft.com/en-us/library/hh678682(v=vs.110).aspx。 - JaredReisinger

10

在我看来,你应该总是有选择使用 Lazy 的理由。根据使用情况,有几种替代方案,并且肯定存在适用于此结构的情况。但不要仅仅因为它很酷而使用它。

例如,在其他答案中的页面选择示例中,我不明白其中的重点。使用 Lazy 列表选择单个元素可以直接使用委托列表或字典而不使用 Lazy 或简单的 switch 语句来完成。

因此,最明显的替代方案是

  • 对于廉价的数据结构或无论如何都需要的结构进行直接实例化
  • 对于某些算法中需要零次到少量次数的事物使用委托
  • 对于应在一段时间内未被使用时释放内存的项目使用某种缓存结构
  • 对于概率相当高的结构稍早开始异步初始化的“未来”结构(例如 Task),在实际使用前消耗空闲 CPU 时间

与此相反,Lazy 经常适用于以下情况:

  • 计算强度大的数据结构
  • 在某些算法中需要零到多次使用,其中零的情况具有显著概率
  • 数据局部于某个方法或类并且可以在不再使用时进行垃圾回收,或者数据应该在整个程序运行时保留在内存中

7
这里并不是负面方面,但对于懒惰的人来说有一个陷阱 :).
懒初始化器类似于静态初始化器。它们只运行一次。如果抛出异常,则会缓存该异常,并且对.Value的后续调用将抛出相同的异常。这是有意设计的,并在文档中提到...http://msdn.microsoft.com/en-us/library/dd642329.aspx:

valueFactory引发的异常被缓存。

因此,下面的代码永远不会返回值:
bool firstTime = true;
Lazy<int> lazyInt = new Lazy<int>(() =>
{
    if (firstTime)
    {
        firstTime = false;
        throw new Exception("Always throws exception the very first time.");
    }

    return 21;
});

int? val = null;
while (val == null)
{
    try
    {
        val = lazyInt.Value;
    }
    catch
    {

    }
}

谢谢@Thilak。这很有趣。我不知道异常会被缓存。 - eandersson
1
@Fuji 这就是为什么MEF团队在MEF 2中添加了ExportFactory和ExportLifetimeContext的原因。请查看http://blogs.msdn.com/b/bclteam/archive/2011/11/17/exportfactory-amp-lt-t-amp-gt-in-mef-2-alok.aspx - Panos Rontogiannis
我觉得现在我需要回去检查一下我的MEF代码。 ;) - eandersson
这只有在某些情况下才是真的:异常缓存的行为取决于发布方法。 - user2864740
如果您想避免异常缓存,请考虑使用 LazyWithNoExceptionCaching - https://dev59.com/klsW5IYBdhLWcg3w2aKK#42567351 - mjwills

6
我主要使用Lazy<T>是因为它在从数据库加载资源时具有并发能力。因此,我摆脱了锁对象和可能产生歧义的锁模式。在我的情况下,ConcurrentDictionary + Lazy 作为值使我的工作更轻松愉快,感谢@Reed Copsey和他的博客帖子

This looks like the following. Instead of calling:

MyValue value = dictionary.GetOrAdd(
                             key, 
                             () => new MyValue(key));

We would instead use a ConcurrentDictionary>, and write:

MyValue value = dictionary.GetOrAdd(
                             key, 
                             () => new Lazy<MyValue>(
                                 () => new MyValue(key)))
                          .Value;

目前还没有发现Lazy<T>的任何缺点。


这真是太棒了。不过我今天有点太活跃了,用完了所有的投票,所以无法为你的答案点赞。;*( - eandersson

4
与任何东西一样,Lazy<T> 可以用于好事或坏事,因此有一个缺点:如果不适当地使用它,可能会导致混淆和挫败感。然而,惰性初始化模式已经存在多年,现在 .NET BCL 有了一个实现,开发人员不需要再次重新发明轮子。更重要的是,MEF 喜欢 Lazy

2

Lazy用于在不需要时保留资源。这个模式相当好,但实现可能没有用。

资源越大,此模式越有用。

使用Lazy类的一个缺点是使用的不透明性。确实,您必须在每个地方维护一个额外的间接引用(.Value)。即使您不需要直接使用它,当您只需要真实类型的实例时,它也被强制加载。

Lazy是用于懒惰开发以提高生产率的,但是过度使用可能会失去这种收益。

如果您有一个真正透明的实现(例如使用代理模式),则可以摆脱缺点,并且在许多情况下非常有用。

并发必须考虑另一方面,而不是默认在您的类型中实现。它必须仅包括在客户端代码或类型助手中以供此概念使用。


2

你在说“Throughout my application”指的是什么?

我认为只有当你不确定值是否会被使用时,才应该使用它。这可能只适用于需要长时间计算的可选参数。这可能包括复杂的计算、文件处理、Web服务、数据库访问等等。

另一方面,为什么要在这里使用Lazy呢?在大多数情况下,你可以直接调用一个方法而不是lazy.Value,而且这样做也没有任何区别。但是,对于程序员来说,在没有Lazy的情况下更简单和明显。

一个明显的好处可能是已经实现了值的缓存,但我认为这并不是一个很大的优势。


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