是否LINQ缓存计算出的值?(Does LINQ cache computed values?)

2019-09-16 17:47发布

假设我有以下代码:

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
");
Enumerable.Range (1, 100)
    .Select (s => X.Elements ()
        .Select (t => Int32.Parse (t.Attribute ("v").Value))
        .Aggregate (s, (t, u) => t * u)
    )
    .ToList ()
    .ForEach (s => Console.WriteLine (s));

什么是.NET运行时居然在这里做什么? 难道解析和转换属性整数各100次,还是足够聪明弄清楚,它应该缓存解析值,而不是重复计算的范围中的每个元素?

此外,我怎么会去搞清楚这样的自己?

在此先感谢您的帮助。

Answer 1:

它已经有一段时间,因为我通过这个代码挖,但IIRC,方式Select的作品是简单地缓存Func您提供它,在一个时间源集合一个运行它。 因此,对于在外部范围内的每个元件,它将运行内Select/Aggregate序列就好像它是第一次。 没有任何内置缓存事情 - 你将不得不以实施自己的表情。

如果你想这出自己,你有三个基本选项:

  1. 编译代码并使用ildasm来查看IL; 这是最准确的,但是,特别是与lambda表达式和封锁,你从IL获得可能看起来一点也不像你所投入的C#编译器的东西。
  2. 使用类似dotPeek反编译System.Linq.dll到C#; 再次,你得到了这些工具的东西可能只有大约类似于原始的源代码,但至少这将是C#(和dotPeek特别做了很好的工作,并且是免费的。)
  3. 我个人的偏好-下载.NET 4.0 参考源 ,寻找自己; 这就是它是:)你必须只相信MS的参考源用来产生二进制文件的实际源相匹配,但我看不出有什么好的理由来怀疑他们。
  4. 正如@AllonGuralnek指出,你可以设置在一行中的特定lambda表达式断点; 将光标移到某个地方拉姆达并按F9的身体里面,它会断点只是拉姆达。 (如果你这样做是错误的,它会突出显示断点颜色在整个行;如果你这样做是正确的,它只会突出拉姆达)。


Answer 2:

LINQ和IEnumerable<T>基于拉 。 这意味着,在一般的LINQ语句的一部分的谓词和操作不被执行,直到值被上拉。 此外,谓词和操作将每个值都被拉时间执行(例如,没有什么秘密缓存回事)。

从拉IEnumerable<T>是由做foreach语句这确实是语法糖通过调用得到一个枚举IEnumerable<T>.GetEnumerator()和重复调用IEnumerator<T>.MoveNext()拉值。

LINQ运营商如ToList() ToArray() ToDictionary()ToLookup()封装了foreach语句,因此这些方法会做一个拉。 同样可以说,大约等运营商Aggregate() Count()First() 这些方法的共同点在于,它们产生必须通过执行来创建一个结果foreach语句。

许多运营商LINQ产生一个新IEnumerable<T>序列。 当一个元件被从所述产生的序列拉动操作者拉动从源序列的一个或多个元件。 该Select()运算符是最明显的例子,但是其他的例子SelectMany() Where() Concat() Union() Distinct() Skip()Take() 这些运营商不要做任何缓存。 当第N再元件由拉Select()就会从源序列第N元件,适用使用所提供的动作,并返回它的投影。 没有什么秘密的事情在这里。

其他LINQ运营商也产生新IEnumerable<T>序列,但它们是由实际拉动整个源序列,做他们的工作,然后产生一个新的序列实现。 这些方法包括Reverse() OrderBy()GroupBy() 然而,当操作者本身被拉这意味着你仍然需要时才执行由操作员进行拉foreach执行任何东西之前循环“底”的LINQ语句。 你可以说,这些运营商使用缓存,因为他们马上拉动整个源序列。 然而,这种缓存每个操作员被反复所以它是一个真正的实现细节,不是东西,会奇迹般地检测到您所申请的同时内置OrderBy()多次操作,以相同的序列。


在您的例子中, ToList()会做一个拉。 在外部动作Select将执行100次。 每次这个动作被执行的Aggregate()会做另一个拉,将解析XML属性。 总共的代码调用Int32.Parse()的200倍。

您可以通过拉动属性一次,而不是在每次迭代改善这一点:

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
")
.Elements ()
.Select (t => Int32.Parse (t.Attribute ("v").Value))
.ToList ();
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList () 
    .ForEach (s => Console.WriteLine (s)); 

现在Int32.Parse()才会被调用2次。 然而,代价是属性值的列表已经被分配,储存和收集垃圾的最终。 (不是大问题时,列表中包含两个元素。)

请注意,如果你忘记了第一ToList()拉动的代码将仍然运行的属性,但具有完全相同的性能特性的原代码。 没有足够的空间用于存储的属性,但它们在每次迭代解析。



文章来源: Does LINQ cache computed values?