假设我有以下代码:
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次中的整数,还是足够聪明,以确定它应该缓存解析后的值而不重复该范围内每个元素的计算?
而且,我怎么会自己搞清楚这样的事情?
提前感谢您的帮助。
答案 0 :(得分:4)
LINQ和IEnumerable<T>
基于拉。这意味着,在拉取值之前,不会执行作为LINQ语句一部分的谓词和操作。此外,每次拉取值时都会执行谓词和操作(例如,没有秘密缓存进行)。
来自IEnumerable<T>
的{{1}}来自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()
,{{1} }和Skip()
。这些运算符不进行任何缓存。当从Take()
拉出第N个元素时,它从源序列中拉出第N个元素,使用提供的动作应用投影并返回它。这里没什么秘密。
其他LINQ运算符也会生成新的Select()
序列,但它们是通过实际拉动整个源序列,完成其工作然后生成新序列来实现的。这些方法包括IEnumerable<T>
,Reverse()
和OrderBy()
。但是,只有当操作符本身被拉动时才会执行操作符的拉取,这意味着在执行任何操作之前,您仍然需要LINQ语句的“{1}}循环”。您可以争辩说这些运算符使用缓存,因为它们会立即拉出整个源序列。但是,每次迭代运算符时都会构建此缓存,因此它实际上是一个实现细节,而不是神奇地检测到您对同一序列多次应用相同的GroupBy()
运算。
在您的示例中,foreach
会进行拉动。外OrderBy()
中的操作将执行100次。每次执行此操作时,ToList()
将执行另一次解析XML属性的拉动。您的代码总共会调用Select
200次。
您可以通过拉一次属性而不是每次迭代来改进这一点:
Aggregate()
现在Int32.Parse()
仅被调用2次。但是,成本是必须分配,存储并最终收集垃圾的属性值列表。 (当列表包含两个元素时,这不是一个大问题。)
请注意,如果您忘记了第一个提取属性的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));
,代码仍将运行但具有与原始代码完全相同的性能特征。没有空间用于存储属性,但是在每次迭代时都会对它们进行解析。
答案 1 :(得分:2)
我挖掘这段代码已经有一段时间但是,IIRC,Select
的工作方式是简单地缓存你提供它的Func
并一次一个地在源集合上运行它。因此,对于外部范围中的每个元素,它将运行内部Select/Aggregate
序列,就像它是第一次一样。没有任何内置缓存 - 你必须自己在表达式中实现它。
如果你想自己解决这个问题,你有三个基本选择:
ildasm
查看IL;它是最准确的,但是,特别是对于lambdas和闭包,你从IL获得的东西可能看起来与你放入C#编译器的内容完全不同。