我经常遇到这样的情况:我想在声明查询的地方对查询进行求值。这通常是因为我需要对它进行多次迭代,计算成本很高。例如:
string raw = "...";
var lines = (from l in raw.Split('\n')
let ll = l.Trim()
where !string.IsNullOrEmpty(ll)
select ll).ToList();
这很好。但是如果我不打算修改结果,那么我也可以调用ToArray()而不是ToList()。
然而,我想知道ToArray()是否通过首先调用ToList()来实现,因此内存效率比只调用ToList()低。
我疯了吗?我是否应该调用ToArray() -在知道内存不会被分配两次的情况下安全可靠?
(七年后……)
其他几个(好的)答案集中在将会发生的微观性能差异上。
这篇文章只是一个补充,以提及由数组(T[])产生的IEnumerator<T>与由List<T>返回的IEnumerator之间存在的语义差异。
最好用例子来说明:
IList<int> source = Enumerable.Range(1, 10).ToArray(); // try changing to .ToList()
foreach (var x in source)
{
if (x == 5)
source[8] *= 100;
Console.WriteLine(x);
}
上面的代码将毫无例外地运行,并产生输出:
1
2
3
4
5
6
7
8
900
10
这表明int[]返回的IEnumarator<int>并不跟踪自枚举器创建以来数组是否被修改过。
Note that I declared the local variable source as an IList<int>. In that way I make sure the C# compiler does not optimze the foreach statement into something which is equivalent to a for (var idx = 0; idx < source.Length; idx++) { /* ... */ } loop. This is something the C# compiler might do if I use var source = ...; instead. In my current version of the .NET framework the actual enumerator used here is a non-public reference-type System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32] but of course this is an implementation detail.
现在,如果我将.ToArray()改为.ToList(),我只得到:
1
2
3
4
5
其次是一个系统。InvalidOperationException爆炸说:
修改集合;枚举操作可能无法执行。
在这种情况下,底层枚举器是公共可变值类型System. collections . generic . list ' 1+ enumerator [System. collections . generic . list]。Int32](在这种情况下,在IEnumerator<int>框内,因为我使用IList<int>)。
综上所述,List<T>生成的枚举数跟踪列表在枚举过程中是否发生变化,而T[]生成的枚举数则没有。因此,在. tolist()和. toarray()之间进行选择时,请考虑此差异。
人们经常添加一个额外的. toarray()或. tolist()来绕过一个在枚举器的生命周期内跟踪它是否被修改的集合。
(如果有人想知道List<>如何跟踪集合是否被修改,这个类中有一个私有字段_version,每当List<>被更新时,它都会被更改。实际上可以通过简单地删除索引器public T this[int index]的set访问器中增加_version的行来改变List<>的这种行为,就像最近在Dictionary<,>中所做的那样,如另一个答案所述。)
(七年后……)
其他几个(好的)答案集中在将会发生的微观性能差异上。
这篇文章只是一个补充,以提及由数组(T[])产生的IEnumerator<T>与由List<T>返回的IEnumerator之间存在的语义差异。
最好用例子来说明:
IList<int> source = Enumerable.Range(1, 10).ToArray(); // try changing to .ToList()
foreach (var x in source)
{
if (x == 5)
source[8] *= 100;
Console.WriteLine(x);
}
上面的代码将毫无例外地运行,并产生输出:
1
2
3
4
5
6
7
8
900
10
这表明int[]返回的IEnumarator<int>并不跟踪自枚举器创建以来数组是否被修改过。
Note that I declared the local variable source as an IList<int>. In that way I make sure the C# compiler does not optimze the foreach statement into something which is equivalent to a for (var idx = 0; idx < source.Length; idx++) { /* ... */ } loop. This is something the C# compiler might do if I use var source = ...; instead. In my current version of the .NET framework the actual enumerator used here is a non-public reference-type System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32] but of course this is an implementation detail.
现在,如果我将.ToArray()改为.ToList(),我只得到:
1
2
3
4
5
其次是一个系统。InvalidOperationException爆炸说:
修改集合;枚举操作可能无法执行。
在这种情况下,底层枚举器是公共可变值类型System. collections . generic . list ' 1+ enumerator [System. collections . generic . list]。Int32](在这种情况下,在IEnumerator<int>框内,因为我使用IList<int>)。
综上所述,List<T>生成的枚举数跟踪列表在枚举过程中是否发生变化,而T[]生成的枚举数则没有。因此,在. tolist()和. toarray()之间进行选择时,请考虑此差异。
人们经常添加一个额外的. toarray()或. tolist()来绕过一个在枚举器的生命周期内跟踪它是否被修改的集合。
(如果有人想知道List<>如何跟踪集合是否被修改,这个类中有一个私有字段_version,每当List<>被更新时,它都会被更改。实际上可以通过简单地删除索引器public T this[int index]的set访问器中增加_version的行来改变List<>的这种行为,就像最近在Dictionary<,>中所做的那样,如另一个答案所述。)
我发现人们在这里做的其他基准测试都有不足,所以这里是我的尝试。如果你发现我的方法有问题,请告诉我。
/* This is a benchmarking template I use in LINQPad when I want to do a
* quick performance test. Just give it a couple of actions to test and
* it will give you a pretty good idea of how long they take compared
* to one another. It's not perfect: You can expect a 3% error margin
* under ideal circumstances. But if you're not going to improve
* performance by more than 3%, you probably don't care anyway.*/
void Main()
{
// Enter setup code here
var values = Enumerable.Range(1, 100000)
.Select(i => i.ToString())
.ToArray()
.Select(i => i);
values.GetType().Dump();
var actions = new[]
{
new TimedAction("ToList", () =>
{
values.ToList();
}),
new TimedAction("ToArray", () =>
{
values.ToArray();
}),
new TimedAction("Control", () =>
{
foreach (var element in values)
{
// do nothing
}
}),
// Add tests as desired
};
const int TimesToRun = 1000; // Tweak this as necessary
TimeActions(TimesToRun, actions);
}
#region timer helper methods
// Define other methods and classes here
public void TimeActions(int iterations, params TimedAction[] actions)
{
Stopwatch s = new Stopwatch();
int length = actions.Length;
var results = new ActionResult[actions.Length];
// Perform the actions in their initial order.
for (int i = 0; i < length; i++)
{
var action = actions[i];
var result = results[i] = new ActionResult { Message = action.Message };
// Do a dry run to get things ramped up/cached
result.DryRun1 = s.Time(action.Action, 10);
result.FullRun1 = s.Time(action.Action, iterations);
}
// Perform the actions in reverse order.
for (int i = length - 1; i >= 0; i--)
{
var action = actions[i];
var result = results[i];
// Do a dry run to get things ramped up/cached
result.DryRun2 = s.Time(action.Action, 10);
result.FullRun2 = s.Time(action.Action, iterations);
}
results.Dump();
}
public class ActionResult
{
public string Message { get; set; }
public double DryRun1 { get; set; }
public double DryRun2 { get; set; }
public double FullRun1 { get; set; }
public double FullRun2 { get; set; }
}
public class TimedAction
{
public TimedAction(string message, Action action)
{
Message = message;
Action = action;
}
public string Message { get; private set; }
public Action Action { get; private set; }
}
public static class StopwatchExtensions
{
public static double Time(this Stopwatch sw, Action action, int iterations)
{
sw.Restart();
for (int i = 0; i < iterations; i++)
{
action();
}
sw.Stop();
return sw.Elapsed.TotalMilliseconds;
}
}
#endregion
你可以在这里下载LINQPad脚本。
结果:
调整上面的代码,你会发现:
当处理较小的数组时,差异就不那么显著了。
在处理整型而不是字符串时,这种差异不太显著。
使用大型结构体而不是字符串通常会花费更多的时间,但并不会真正改变比例。
这与投票最多的答案的结论一致:
除非您的代码经常生成许多大型数据列表,否则不太可能注意到性能上的差异。(当创建1000个包含100K字符串的列表时,只有200ms的差异。)
ToList()始终运行得更快,如果不打算长时间保留结果,那么它是一个更好的选择。
更新
@JonHanna指出,根据Select的实现,ToList()或ToArray()实现可以提前预测结果集合的大小。将上面代码中的. select (i => i)替换为Where(i => true)会产生非常相似的结果,并且更有可能这样做,而不管. net实现如何。
我同意@mquander的观点,性能差异应该是微不足道的。但是,我想对它进行基准测试,所以我这样做了——结果是微不足道的。
Testing with List<T> source:
ToArray time: 1934 ms (0.01934 ms/call), memory used: 4021 bytes/array
ToList time: 1902 ms (0.01902 ms/call), memory used: 4045 bytes/List
Testing with array source:
ToArray time: 1957 ms (0.01957 ms/call), memory used: 4021 bytes/array
ToList time: 2022 ms (0.02022 ms/call), memory used: 4045 bytes/List
每个源数组/列表有1000个元素。所以你可以看到时间和记忆的差异都可以忽略不计。
我的结论是:您还可以使用ToList(),因为List<T>提供了比数组更多的功能,除非几个字节的内存确实对您很重要。