LINQ 并且IEnumerable<T>
是基于拉的。这意味着作为 LINQ 语句一部分的谓词和操作通常在提取值之前不会执行。此外,每次拉取值时都会执行谓词和操作(例如,没有秘密缓存正在进行)。
从 an中提取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()
。这些操作符不做任何缓存。当从 aSelect()
中拉出第 N 个元素时,它会从源序列中拉出第 N 个元素,使用提供的操作应用投影并返回它。这里没有什么秘密。
其他 LINQ 运算符也产生新IEnumerable<T>
序列,但它们是通过实际拉取整个源序列、完成它们的工作然后产生新序列来实现的。这些方法包括Reverse()
和。但是,操作员完成的拉取操作仅在操作员本身被拉取时执行,这意味着在执行任何操作之前您仍然需要在 LINQ 语句的“末尾”循环。您可能会争辩说这些运算符使用缓存,因为它们会立即提取整个源序列。但是,每次迭代操作符时都会构建此缓存,因此它实际上是一个实现细节,而不是会神奇地检测到您正在对同一序列多次应用相同的操作。OrderBy()
GroupBy()
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()
提取属性的代码,代码仍将运行,但性能特征与原始代码完全相同。没有空间用于存储属性,但在每次迭代时都会对其进行解析。