LINQ 和IEnumerable<T> 是基于拉的。这意味着作为 LINQ 语句一部分的谓词和操作通常在提取值之前不会执行。此外,每次提取值时都会执行谓词和操作(例如,没有秘密缓存正在进行)。
从IEnumerable<T> 中拉取是通过foreach 语句完成的,该语句实际上是通过调用IEnumerable<T>.GetEnumerator() 并反复调用IEnumerator<T>.MoveNext() 来获取枚举值的语法糖。
ToList()、ToArray()、ToDictionary() 和 ToLookup() 等 LINQ 运算符包装了 foreach 语句,因此这些方法将执行拉取操作。对于Aggregate()、Count() 和First() 等运算符也是如此。这些方法的共同点是它们产生一个必须通过执行foreach 语句来创建的结果。
许多 LINQ 运算符产生一个新的IEnumerable<T> 序列。当从结果序列中拉出一个元素时,运算符会从源序列中拉出一个或多个元素。 Select() 运算符是最明显的例子,但其他例子是 SelectMany()、Where()、Concat()、Union()、Distinct()、Skip() 和 Take()。这些操作符不做任何缓存。当从Select() 中提取第 N 个元素时,它会从源序列中提取第 N 个元素,使用提供的操作应用投影并返回它。这里没有什么秘密。
其他 LINQ 运算符也生成新的IEnumerable<T> 序列,但它们是通过实际拉取整个源序列、完成它们的工作然后生成新序列来实现的。这些方法包括Reverse()、OrderBy() 和GroupBy()。但是,操作员完成的拉取操作仅在操作员本身被拉取时执行,这意味着在执行任何操作之前,您仍然需要在 LINQ 语句的“末尾”有一个foreach 循环。您可能会争辩说这些运算符使用缓存,因为它们会立即提取整个源序列。但是,每次迭代运算符时都会构建此缓存,因此它实际上是一个实现细节,而不是会神奇地检测到您正在对同一序列多次应用相同的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(),代码仍将运行,但性能特征与原始代码完全相同。不使用空间来存储属性,但在每次迭代时都会对其进行解析。