假设我有以下代码:
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 次,还是它足够聪明地确定它应该缓存解析的值而不是对范围内的每个元素重复计算?
此外,我该如何自己解决这样的问题?
预先感谢您的帮助。
最佳答案
LINQ 和 IEnumerable<T>
是基于拉动的。这意味着在提取值之前,通常不会执行作为 LINQ 语句一部分的谓词和操作。此外,每次提取值时都会执行谓词和操作(例如,没有进行 secret 缓存)。
从 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()
.这些运算符不做任何缓存。然后从 Select()
中提取第 N 个元素它从源序列中提取第 N 个元素,使用提供的操作应用投影并返回它。这里没有什么 secret 。
其他 LINQ 运算符也产生新的 IEnumerable<T>
序列,但它们是通过实际提取整个源序列、完成它们的工作然后生成新序列来实现的。这些方法包括 Reverse()
, OrderBy()
和 GroupBy()
.但是,运算符(operator)完成的拉动仅在运算符(operator)本身被拉动时执行,这意味着您仍然需要 foreach
在执行任何内容之前,在 LINQ 语句的“末尾”循环。您可能会争辩说这些运算符使用缓存是因为它们会立即提取整个源序列。但是,每次迭代运算符时都会构建此缓存,因此它实际上是一个实现细节,而不是神奇地检测到您正在应用相同的 OrderBy()
的东西。对同一个序列多次操作。
在您的示例中,ToList()
会做一个拉。外层的 Action 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()
提取代码仍将运行但具有与原始代码完全相同的性能特征的属性。没有空间用于存储属性,但在每次迭代时解析它们。
关于c# - LINQ 是否缓存计算值?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10308381/