假设我有以下代码:
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 语句一部分的谓词和操作。此外,每次拉取值时,谓词和操作都将执行(例如,没有秘密缓存正在进行)。
从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 个元素,使用提供的操作应用投影并返回它。这里没有什么秘密。
其他 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()
,代码仍将运行,但具有与原始代码完全相同的性能特征。没有空间用于存储属性,但在每次迭代时都会分析它们。
自从我深入研究这段代码以来已经有一段时间了,但是,IIRC,Select
的工作方式是简单地缓存您提供它Func
,然后一次在源集合上运行它。因此,对于外部范围中的每个元素,它将像第一次一样运行内部Select/Aggregate
序列。没有任何内置缓存正在进行 - 您必须在表达式中自己实现它。
如果你想自己弄清楚,你有三个基本选择:
- 编译代码并使用
ildasm
查看 IL;这是最准确的,但对于 lambda 和闭包,从 IL 获得的内容可能与您放入 C# 编译器的内容完全不同。 - 使用dotPeek之类的东西将System.Linq.dll反编译为C#;同样,你从这些工具中得到的东西可能只是与原始源代码大致相似,但至少它将是C#(特别是dotPeek做得很好,而且是免费的。
- 我个人的偏好 - 下载 .NET 4.0 参考源并自行查找;这就是它的用途:)您只需要相信 MS 引用源与用于生成二进制文件的实际源匹配,但我认为没有任何充分的理由怀疑它们。
- 正如@AllonGuralnek所指出的,您可以在一行内对特定的lambda表达式设置断点;将光标放在lambda主体内的某个位置,然后按F9,它将仅对lambda断点。(如果你做错了,它将以断点颜色突出显示整行;如果你做对了,它只会突出显示 lambda。