7

假设我有以下代码:

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 次解析属性并将其转换为整数,还是足够聪明地确定它应该缓存解析的值而不是对范围内的每个元素重复计算?

此外,我将如何自己弄清楚这样的事情?

在此先感谢您的帮助。

4

2 回答 2

4

LINQ 并且IEnumerable<T>基于拉的。这意味着作为 LINQ 语句一部分的谓词和操作通常在提取值之前不会执行。此外,每次拉取值时都会执行谓词和操作(例如,没有秘密缓存正在进行)。

从 an中提取IEnumerable<T>是由语句完成的,该语句实际上是通过调用并反复调用来foreach获取枚举器以提取值的语法糖。IEnumerable<T>.GetEnumerator()IEnumerator<T>.MoveNext()

LINQ 运算符如ToList(),和包装一个语句ToArray(),因此这些方法将执行拉取操作。对于,和等运算符也是如此。这些方法的共同点是它们产生一个必须通过执行语句创建的结果。ToDictionary()ToLookup()foreachAggregate()Count()First()foreach

许多 LINQ 运算符产生一个新IEnumerable<T>序列。当从结果序列中拉出一个元素时,运算符会从源序列中拉出一个或多个元素。运算符是最Select()明显的例子,但其他例子是SelectMany(), Where(), Concat(), Union(), Distinct(),Skip()Take()。这些操作符不做任何缓存。当从 aSelect()中拉出第 N 个元素时,它会从源序列中拉出第 N 个元素,使用提供的操作应用投影并返回它。这里没有什么秘密。

其他 LINQ 运算符也产生新IEnumerable<T>序列,但它们是通过实际拉取整个源序列、完成它们的工作然后产生新序列来实现的。这些方法包括Reverse()和。但是,操作员完成的拉取操作仅在操作员本身被拉取时执行,这意味着在执行任何操作之前您仍然需要在 LINQ 语句的“末尾”循环。您可能会争辩说这些运算符使用缓存,因为它们会立即提取整个源序列。但是,每次迭代操作符时都会构建此缓存,因此它实际上是一个实现细节,而不是会神奇地检测到您正在对同一序列多次应用相同的操作。OrderBy()GroupBy()foreachOrderBy()


在您的示例中,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()提取属性的代码,代码仍将运行,但性能特征与原始代码完全相同。没有空间用于存储属性,但在每次迭代时都会对其进行解析。

于 2012-04-25T09:23:22.920 回答
2

自从我研究这段代码以来已经有一段时间了,但是 IIRC 的工作方式Select是简单地缓存Func您提供的代码并一次在源集合上运行它。因此,对于外部范围内的每个元素,它将Select/Aggregate像第一次一样运行内部序列。没有任何内置缓存正在进行 - 您必须自己在表达式中实现它。

如果你想自己解决这个问题,你有三个基本选择:

  1. 编译代码并使用ildasm查看IL;它是最准确的,但是,尤其是对于 lambda 和闭包,从 IL 获得的内容可能与您放入 C# 编译器的内容完全不同。
  2. 使用 dotPeek 之类的东西将 System.Linq.dll 反编译成 C#;同样,您从这些工具中得到的东西可能只是与原始源代码大致相似,但至少它将是 C#(特别是 dotPeek 做得很好,而且是免费的。)
  3. 我个人的偏好——下载 .NET 4.0参考源并自己寻找;这就是它的用途:) 您必须相信 MS,参考源与用于生成二进制文件的实际源相匹配,但我认为没有任何充分的理由怀疑它们。
  4. 正如@AllonGuralnek 所指出的,您可以在一行内的特定 lambda 表达式上设置断点;将光标放在 lambda 主体内的某个位置,然后按 F9,它将仅在 lambda 处设置断点。(如果操作错误,它将以断点颜色突出显示整行;如果操作正确,它将仅突出显示 lambda。)
于 2012-04-25T02:43:48.290 回答