通过不缓存结果的方式实现LINQ有什么好处?


20

对于使用LINQ弄湿脚的人来说,这是一个众所周知的陷阱:

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

这将显示“ False”,因为对于提供的用于创建原始集合的每个名称,select函数将不断重新评估,并且Record重新创建结果对象。要解决此问题,ToList可以在的末尾添加一个简单的调用GenerateRecords

微软希望通过这种方式获得哪些好处?

为什么实现不简单地将结果缓存在内部数组中?发生的事情的一个特定部分可能是延迟执行,但是如果没有这种行为,仍然可以实现。

一旦评估了LINQ返回的集合的给定成员,不保留内部引用/副本,而是重新计算相同的结果作为默认行为,将提供什么优势?

在逻辑上需要一遍又一遍地重新计算集合的同一成员的情况下,似乎可以通过可选参数来指定,而默认行为可以这样做。此外,通过延迟执行而获得的速度优势最终会被持续重新计算相同结果所需的时间削减。最后,对于LINQ的新手来说,这是一个令人困惑的障碍,并且可能最终导致任何人的程序中的细微错误。

这有什么好处?为什么微软做出这个看似非常故意的决定?


1
只需在GenerateRecords()方法中调用ToList()。 return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); 这样就可以得到“缓存的副本”。问题解决了。
罗伯特·哈维

1
我知道,但我想知道为什么他们首先要这样做。
Panzercrisis

11
因为惰性评估具有显着的好处,但最重要的是“哦,顺便说一句,自您上次请求以来,此记录已更改;这是新版本”,这正是您的代码示例所演示的。
罗伯特·哈维

我可以发誓,在过去的6个月中,我在这里阅读了几乎相同措词的问题,但现在找不到。我能找到的最接近的
年份

29
我们有一个没有过期策略的缓存名称:“内存泄漏”。我们有一个没有无效策略的缓存名称:“错误服务器场”。如果您不打算提出适用于所有可能的LINQ查询的始终正确的到期和失效策略,那么您的问题就可以自己回答。
埃里克·利珀特

Answers:


51

通过不缓存结果的方式实现LINQ有什么好处?

缓存结果根本不适合所有人。只要您有少量数据,就可以了。对你有好处。但是,如果您的数据大于RAM怎么办?

它与LINQ无关,但与整个IEnumerable<T>接口无关。

这是File.ReadAllLinesFile.ReadLines之间的区别。一个将整个文件读入RAM,另一个将逐行将其提供给您,因此您可以处理大型文件(只要它们换行符即可)。

您可以轻松地高速缓存一切你通过物化的程序调用要么需要高速缓存.ToList().ToArray()在其上。但是,我们这些谁也不会想要缓存的话,我们有机会没有这样做。

并有一个相关的说明:如何缓存以下内容?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

你不能。这就是为什么如此IEnumerable<T>存在。


2
如果您的最后一个示例是一个实际的无穷级数(例如Fibonnaci),而不仅仅是一个无穷无尽的零字符串,那么您的最后一个示例将更引人注目,这并不是特别有趣。
罗伯特·哈维

23
@RobertHarvey没错,我只是觉得当没有逻辑可以理解的时候,它是无穷无尽的零流比较容易。
nvoigt

2
int i=1; while(true) { i++; yield fib(i); }
罗伯特·哈维

2
我想到的示例是Enumerable.Range(1,int.MaxValue)-很容易确定要使用多少内存的下限。
克里斯(Chris

4
我所看到的另一件事while (true) return ...while (true) return _random.Next();生成无限的随机数流。
克里斯(Chris

24

微软希望通过这种方式获得哪些好处?

正确吗?我的意思是,可枚举的核心可以在两次调用之间改变。缓存它会产生错误的结果,并打开整个“何时/如何使该缓存失效?”蠕虫病毒。

如果你认为LINQ最初被设计为做LINQ到数据源(如实体框架,或直接从SQL)的一种手段,可枚举要改变,因为这就是数据库

最重要的是,存在单一责任原则的问题。使一些查询代码可以工作并在其上构建缓存要比构建查询和缓存然后删除缓存的代码容易得多。


3
这可能是值得一提的ICollection存在,可能的行为方式OP期待IEnumerable的行为
Caleth

如果您使用IEnumerable <T>来读取打开的数据库游标,则如果您使用带有ACID事务的数据库,则结果不会改变。
道格

4

因为LINQ是并且从一开始就打算是在函数式编程语言中流行的Monad模式通用实现,并且Monad不必总是在给定相同调用序列的情况下始终产生相同的值(实际上,它的使用正是由于这种特性,函数式编程中的“类”才流行起来,从而避免了纯函数的确定性行为。


4

尚未提及的另一个原因是,可以在不创建垃圾中间结果的情况下连接不同的过滤器和转换。

以这个为例:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

如果LINQ方法立即计算出结果,我们将有3个集合:

  • 结果在哪里
  • 选择结果
  • 按结果分组

其中我们只关心最后一个。保存中间结果是没有意义的,因为我们无法访问它们,我们只想知道已经按年份过滤和分组的汽车。

如果需要保存这些结果中的任何一个,则解决方案很简单:将调用分开并对其进行调用.ToList(),然后将其保存在变量中。


顺便提一句,在JavaScript中,Array方法实际上会立即返回结果,如果不小心,可能导致更多的内存消耗。


3

从根本上说,这种代码-把一个Guid.NewGuid ()一个内部Select说法-是非常可疑。这肯定是某种代码的味道!

从理论上讲,我们不一定希望Select语句创建新数据,而是检索现有数据。尽管“选择”从多个源联接数据以产生不同形状的联接内容甚至计算其他列是合理的,但我们仍可能希望它既实用又纯粹。把NewGuid ()里面使得非功能性及非纯的。

可以将数据的创建与选择分离开来,并放入某种创建操作中,以便选择可以保持纯净和可重复使用,否则选择应该只进行一次并包装/保护即可。是.ToList ()建议。

但是,要明确地说,在我看来,问题似乎在于选择内部的创造混合,而不是缺乏缓存。把NewGuid()选择内部出现对我而言,编程模型的不恰当的混合。


0

延迟执行使编写LINQ代码的人(确切地说,使用IEnumerable<T>)可以显式选择是否立即计算结果并将其存储在内存中。换句话说,它允许程序员选择最适合其应用程序的计算时间与存储空间的权衡。

可以争辩说,大多数应用程序立即需要结果,因此这应该是LINQ的默认行为。但是List<T>.ConvertAll自框架创建以来,还有许多其他API(例如)提供了这种行为,但是在引入LINQ之前,还没有办法推迟执行。正如其他答案所表明的那样,这是启用某些类型的计算的先决条件,否则在使用立即执行时将无法(通过耗尽所有可用存储)。

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.