4

我一直在我正在编写的应用程序中大量使用 LINQ 查询,而我一直遇到的一种情况是必须将 LINQ 查询结果转换为列表以进行进一步处理(我有我的理由想要的清单)。

我想更好地了解此列表转换中发生的情况,以防效率低下,因为我现在已经反复使用它。所以,假设我执行这样的一行:

var matches = (from x in list1 join y in list2 on x equals y select x).ToList();

问题:

  1. 除了创建一个新列表及其对查询返回的 Enumerable 中的元素的引用之外,这里是否还有任何开销?

  2. 你会认为这是低效的吗?

  3. 有没有办法让 LINQ 查询直接生成列表以避免在这种情况下需要转换?

4

5 回答 5

5

好吧,它会创建数据的副本。这可能效率低下 - 但这取决于发生了什么。如果你最后需要一个List<T>List<T>通常会接近你得到的效率。一个例外是,如果您进行转换并且源已经是一个列表 - 那么使用ConvertAll会更有效,因为它可以创建正确大小的支持数组开始。

如果您需要对数据进行流式传输——例如,您只需对其foreach进行操作,并采取不影响原始数据源的操作——那么调用ToList绝对是低效率的潜在来源。它将强制对整个list1进行评估 - 如果这是一个延迟评估的序列(例如“来自随机数生成器的前 1,000,000 个值”),那就不好了。请注意,当您进行连接时,无论如何list2都会在您尝试从序列中提取第一个值时进行评估(无论是否为了填充列表)。

您可能想阅读我的Edulinq 帖子,ToList以了解在后台发生了什么——至少在一种可能的实现中。

于 2012-07-24T18:15:37.350 回答
1
  1. 除了那些已经被你指定的那些之外,没有任何其他的overhed。

  2. 我会说是的,但这取决于具体的应用场景。顺便说一句,通常最好避免额外的调用。(我认为这很明显)。

  3. 恐怕不是。LINQ query返回一个数据序列,这可能一个无限序列。转换为您使其成为 finit,还可以进行索引访问,这在sequencestreamList<T>中是不可能的。

建议:避免出现需要List<T>. 顺便说一句,如果您需要它,请在当前时刻尽可能少地推送您需要的数据。

希望这可以帮助。

于 2012-07-24T18:18:07.890 回答
1

除了已经说过的内容之外,如果您要加入的最初两个列表已经非常大,那么创建第三个(创建两者的“交集”)可能会导致内存不足错误。如果您只是迭代 LINQ 语句的结果,您将显着减少内存使用量。

于 2012-07-24T18:25:04.070 回答
0

Enumerable.ToList(source)本质上只是对new List(source).

此构造函数将测试 source 是否为ICollection<T>,如果它分配了适当大小的数组。在其他情况下,即源是 LINQ 查询的大多数情况下,它将分配一个具有默认初始容量(四个项目)的数组,并根据需要通过将容量加倍来增长它。每次容量翻倍时,都会分配一个新数组,并将旧数组复制到新数组中。

如果您的列表有很多项目(我们可能至少要谈论数千个),这可能会带来一些开销。一旦列表增长超过 85 KB,开销就会很大,因为它随后会分配到未压缩的大对象堆上,并且可能会遭受内存碎片的影响。请注意,我指的是列表中的数组。如果T是引用类型,则该数组仅包含引用,而不包含实际对象。这些对象不计入 85 KB 限制。

如果您可以准确估计序列的大小(在这种情况下,高估一点比低估一点要好),您可以消除一些这种开销。例如,如果您只.Select()在实现的东西上运行运算符ICollection<T>,您就知道输出列表的大小。

在这种情况下,此扩展方法将减少此开销:

public static List<T> ToList<T>(this IEnumerable<T> source, int initialCapacity)
{
    // parameter validation ommited for brevity

    var result = new List<T>(initialCapacity);

    foreach (T item in source)
    {
        result.Add(item);
    }

    return result;
}

在某些情况下,您创建的列表只是要替换已经存在的列表,例如以前运行的列表。在这些情况下,如果您重用旧列表,则可以避免相当多的内存分配。但是,只有在您无法同时访问该旧列表时,这才有效,如果新列表通常比旧列表小得多,我不会这样做。如果是这种情况,您可以使用此扩展方法:

public static void CopyToList<T>(this IEnumerable<T> source, List<T> destination)
{
    // parameter validation ommited for brevity

    destination.Clear();

    foreach (T item in source)
    {
        destination.Add(item);
    }
}

话虽这么说,我会考虑.ToList()效率低下吗?不,如果您有内存,并且您将重复使用该列表,要么对它进行大量随机索引,要么对其进行多次迭代。

现在回到你的具体例子:

var matches = (from x in list1 join y in list2 on x equals y select x).ToList(); 

以其他方式执行此操作可能更有效,例如

var matches = list1.Intersect(list2).ToList();

如果 list1 和 list2 不包含重复项,这将产生相同的结果,并且如果 list2 很小,则非常有效。

不过,像往常一样,真正了解的唯一方法是使用典型的工作负载进行测量。

于 2012-07-24T18:59:33.693 回答
0
  1. 大多数开销发生在创建列表之前,例如与数据库的连接,将数据获取到
    适配器,对于 var 类型,.NET 需要确定它的数据类型/结构......

  2. 效率是非常相对的术语。对于不擅长 SQL 的程序员来说,1 中详述的开销是高效、更快的开发(相对于旧的 ADO)。

  3. 另一方面,LINQ 可以从数据库本身调用过程,这已经更快了。我建议你进行下一个测试:

    • 在最大数量的数据上运行您的程序并测量时间。
    • 使用一些数据库程序将数据导出到文件(如 XML、CSV、....)并尝试从该文件构建您的列表并测量时间。然后您可以查看差异是否显着。但第二种方式对程序员来说效率较低,但可以减少运行时间。
于 2012-07-24T19:00:10.543 回答