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 很小,则非常有效。
不过,像往常一样,真正了解的唯一方法是使用典型的工作负载进行测量。