1

我正在使用一些 LINQ 选择的东西来创建一些集合,这些集合返回IEnumerable<T>.

在我的情况下,我需要一个List<T>,所以我将结果传递给List<T>' 的构造函数来创建一个。

我想知道这样做的开销。我收藏的物品通常以数百万计,所以我需要考虑这一点。

我假设,如果IEnumerable<T>contains ValueTypes,这是最差的表现。

我对吗?Ref类型呢?不管怎样,还有List<T>.Add一百万次通话的成本,对吧?

有什么办法可以解决这个问题?就像我可以使用扩展方法“重载”LINQ Select 等方法吗?

4

6 回答 6

6

最好避免需要列表。如果您可以使用 IEnumerable<T> 保留您的调用者,您将省去一些麻烦。

LINQ 的 ToList() 将获取您的枚举,并使用 List<T>(IEnumerable<T>) 构造函数直接从中构造一个新的 List<T> 。这与您自己制作列表一样,在性能方面(尽管 LINQ 也会进行空值检查)。

如果您自己添加元素,请使用 AddRange 方法而不是 Add。ToList() 与 AddRange 非常相似(因为它使用采用 IEnumerable<T> 的构造函数),在这种情况下,这通常是您最好的选择,性能方面。

于 2009-06-22T16:58:18.987 回答
6

不,对于元素类型是值类型没有特别的惩罚,假设你使用IEnumerable<T>而不是IEnumerable. 你不会得到任何拳击。

如果您实际上事先知道结果的大小(结果Select可能不会),您可能需要考虑创建具有该缓冲区大小的列表,然后使用AddRange添加值。否则,列表每次填充它时都必须调整其缓冲区的大小。

例如,不要这样做:

Foo[] foo = new Foo[100];
IEnumerable<string> query = foo.Select(foo => foo.Name);
List<string> queryList = new List<string>(query);

你可能会这样做:

Foo[] foo = new Foo[100];
IEnumerable<string> query = foo.Select(x => x.Name);
List<string> queryList = new List<string>(foo.Length);
queryList.AddRange(query);

知道调用Select将产生与原始查询源长度相同的序列,但据我所知,执行环境中没有任何信息。

于 2009-06-22T17:02:45.050 回答
1

不要将 IEnumerable 传递给 List 构造函数。IEnumerable 有一个 ToList() 方法,它不可能做得比这更糟,并且有更好的语法(恕我直言)。

也就是说,这只会将您的问题的答案更改为“取决于” - 特别是,它取决于 IEnumerable 实际上在幕后是什么。如果它碰巧已经是一个 List,那么 ToList实际上是免费的,当然会比其他类型快得多。它仍然不是超快。

当然,解决这个问题的最好方法是尝试弄清楚如何在 IEnumerable 而不是 List 上进行处理。那可能是不可能的。


编辑:评论中的一些人正在争论 ToList() 在 List 上调用时是否实际上会比不调用时更快,以及 ToList() 是否会比列表构造函数更快。在这一点上,猜测变得毫无意义,所以这里有一些代码:

using System;
using System.Linq;
using System.Collections.Generic;

public static class ToListTest
{
    public static int Main(string[] args)
    {
        List<int> intlist = new List<int>();
        for (int i = 0; i < 1000000; i++)
            intlist.Add(i);

        IEnumerable<int> intenum = intlist;

        for (int i = 0; i < 1000; i++)
        {
            List<int> foo = intenum.ToList();
        }

        return 0;
    }
}

使用实际上是 List 的 IEnumerable 运行此代码比将其替换为 LinkedList 或 Stack 快 6-10 倍(在我的 pokey 2.4 GHz P4 上,使用 Mono 1.2.6)。可以想象,这可能是由于 ToList() 与 LinkedList 或 Stack 枚举的特定实现之间的一些不幸的交互,但至少要点仍然存在:速度将取决于 IEnumerable 的底层类型。也就是说,即使使用 List 作为源,我仍然需要 6 秒才能进行 1000 次 ToList() 调用,所以它远非免费。

下一个问题是 ToList() 是否比 List 构造函数更智能。答案是否定的:List 构造函数与 ToList() 一样快。事后看来,Jon Skeet 的推理是有道理的——我只是忘记了 ToList() 是一种扩展方法。我仍然(非常)在语法上更喜欢 ToList() ,但没有使用它的性能理由。

所以简短的版本是最好的答案仍然是“如果可以避免的话,不要转换为列表”。除此之外,实际性能将在很大程度上取决于 IEnumerable 的实际情况,但充其量它会很缓慢,而不是冰冷。我已经修改了我的原始答案以反映这一点。

于 2009-06-22T16:59:31.090 回答
1

一般来说,返回的方法IEnumerable不必在实际需要项目之前评估任何项目。因此,理论上,当您返回时,您的IEnumerable任何物品都不需要存在。

因此,创建列表意味着您确实需要评估项目,获取它们并将它们放置在内存中的某个位置(至少是它们的引用)。对此无能为力 - 如果您真的需要一份清单。

于 2009-06-22T17:04:58.027 回答
1

许多其他响应者已经提供了有关如何提高将 a 复制IEnumerable<T>到 a的性能的想法List<T>——我认为在这方面可以添加的内容不多。

但是,根据您所描述的内容,您需要对结果进行处理,以及完成后您会摆脱列表的事实(我认为这意味着中间结果并不有趣) - 您可能需要考虑是否真的需要实现一个List<T>.

List<T>与其创建一个并对该列表的内容进行操作,不如考虑编写一个惰性扩展方法来IEnumerable<T>执行相同的处理逻辑。我自己在很多情况下都这样做过,在使用[yield return][1]编译器支持的语法时,在 C# 中编写这样的逻辑并不是那么糟糕。

如果您要做的只是访问结果中的每个项目并从中收集一些信息,则此方法非常有效。通常,您需要做的只是按需访问集合中的每个元素,对其进行一些处理,然后继续。这种方法通常比创建集合的副本只是为了迭代它更具可扩展性和性能。

现在,由于其他原因,此建议可能对您不起作用,但值得考虑作为寻找最有效方法来实现非常大的列表的替代方法。

于 2009-06-22T17:32:15.980 回答
1

通过阅读各种评论和问题,我得到以下要求

对于一个数据集合,您需要遍历该集合,过滤掉一些对象,然后对剩余的对象执行一些转换。如果是这种情况,您可以执行以下操作:

var result = from item in collection
             where item.Id > 10 //or some more sensible condition
             select Operation(item);

如果您需要执行更多过滤和转换,您可以嵌套您的 LINQ 查询,例如

var result = from filteredItem in (from item in collection
                                  where item.Id > 10 //or some more sensible condition
                                  select Operation(item))
                 where filteredItem.SomePropertyAvailableAfterFirstTransformation == "new"
                 select SecondTransfomation(filteredItem);
于 2009-06-23T11:12:20.513 回答