大多数情况下,答案是没有关系。循环中的项目数量(即使是您认为“大量”的项目,例如数千个)也不会对代码产生影响。
当然,如果您在特定情况下确定这是瓶颈,请务必解决它,但首先必须确定瓶颈。
话虽如此,每种方法都有一些需要考虑的因素,我将在此处概述。
让我们首先定义一些内容:
这里是每个测试所需的一些帮助程序:
MyObject
类:
public class MyObject
{
public int IntValue { get; set; }
public double DoubleValue { get; set; }
}
一种创建任意长度的
MyClass
实例
List<T>
的方法:
public static List<MyObject> CreateList(int items)
{
if (items < 0)
throw new ArgumentOutOfRangeException("items", items,
"The items parameter must be a non-negative value.");
return Enumerable.Range(0, items).
Select(i => new MyObject { IntValue = i, DoubleValue = i }).
ToList();
}
对列表中每个项目执行的操作(因为方法2使用委托,需要调用某个东西来衡量影响):
public static void MyObjectAction(MyObject obj, TextWriter writer)
{
Debug.Assert(obj != null);
Debug.Assert(writer != null);
writer.WriteLine("MyObject.IntValue: {0}, MyObject.DoubleValue: {1}",
obj.IntValue, obj.DoubleValue);
}
创建一个写入到
null Stream
(也就是一个数据汇)的
TextWriter
方法:
public static TextWriter CreateNullTextWriter()
{
return new StreamWriter(Stream.Null);
}
让我们将项目数量固定为一百万(1,000,000),这应该足够高以强制确保它们的性能影响大致相同:
// The number of items to test.
public const int ItemsToTest = 1000000;
让我们开始介绍方法:
方法一:foreach
下面是代码:
foreach(var item in myList)
{
//Do stuff
}
编译成以下内容:
using (var enumerable = myList.GetEnumerable())
while (enumerable.MoveNext())
{
var item = enumerable.Current;
}
这里有很多内容。你会看到方法调用(编译器在这种情况下尊重鸭子类型,所以它可能与IEnumerator<T>
或IEnumerator
接口无关),而且// Do stuff
被提升到了while结构中。
以下是用于衡量性能的测试:
[TestMethod]
public void TestForEachKeyword()
{
List<MyObject> list = CreateList(ItemsToTest);
using (TextWriter writer = CreateNullTextWriter())
{
Stopwatch s = Stopwatch.StartNew();
foreach (var item in list)
{
MyObjectAction(item, writer);
}
Debug.WriteLine("Foreach loop ticks: {0}", s.ElapsedTicks);
}
}
输出:
Foreach循环计时: 3210872841
方法2: List<T>
上的.ForEach
方法
List<T>
上的.ForEach
方法的代码大致如下:
public void ForEach(Action<T> action)
{
for (int index = 0; index < Count; ++index)
{
action(this[index]);
}
}
请注意,这与方法4在功能上是等效的,唯一的例外是被提升到
for
循环中的代码被作为委托传递。这需要解除引用才能执行需要执行的代码。虽然委托的性能从.NET 3.0开始有所改善,但仍存在开销。
然而,这个开销可以忽略不计。测试性能的方法:
[TestMethod]
public void TestForEachMethod()
{
List<MyObject> list = CreateList(ItemsToTest);
using (TextWriter writer = CreateNullTextWriter())
{
Stopwatch s = Stopwatch.StartNew();
list.ForEach(i => MyObjectAction(i, writer));
Debug.WriteLine("ForEach method ticks: {0}", s.ElapsedTicks);
}
}
输出:
ForEach方法的时钟滴答声: 3135132204
这实际上比使用foreach
循环快了约7.5秒。这并不完全令人惊讶,因为它使用直接数组访问而不是使用IEnumerable<T>
。
但请记住,这相当于每个项节省0.0000075740637秒。对于小型列表来说,这并不值得。
方法3:while (myList.MoveNext())
如方法1所示,这正是编译器所做的事情(还有using
语句的添加,这是一个好习惯)。在这里,通过自己展开编译器将生成的代码,你没有获得任何东西。
出于好奇,我们还是试一下:
[TestMethod]
public void TestEnumerator()
{
List<MyObject> list = CreateList(ItemsToTest);
using (TextWriter writer = CreateNullTextWriter())
using (IEnumerator<MyObject> enumerator = list.GetEnumerator())
{
Stopwatch s = Stopwatch.StartNew();
while (enumerator.MoveNext())
{
MyObjectAction(enumerator.Current, writer);
}
Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
}
}
输出结果:
枚举循环计时:3241289895
方法四:for
在这个特定的情况下,你会获得一些速度优势,因为列表索引器直接访问底层数组以执行查找(顺便说一下,这是一种实现细节,没有什么可以保证它不可能是支持List<T>
的树结构)。
[TestMethod]
public void TestListIndexer()
{
List<MyObject> list = CreateList(ItemsToTest);
using (TextWriter writer = CreateNullTextWriter())
{
Stopwatch s = Stopwatch.StartNew();
for (int i = 0; i < list.Count; ++i)
{
MyObject item = list[i];
MyObjectAction(item, writer);
}
Debug.WriteLine("List indexer loop ticks: {0}", s.ElapsedTicks);
}
}
输出:
列表索引器循环计数:3039649305
然而,这可能会对数组产生影响。编译器可以将数组展开以一次处理多个项目。
与其在十个项目的循环中执行十次单个项目的迭代,编译器可以将其展开为在十个项目的循环中执行五次双项目的迭代。
然而,我不能确定这是否真的发生了(我必须查看IL和编译后的IL输出)。
以下是测试:
[TestMethod]
public void TestArray()
{
MyObject[] array = CreateList(ItemsToTest).ToArray();
using (TextWriter writer = CreateNullTextWriter())
{
Stopwatch s = Stopwatch.StartNew();
for (int i = 0; i < array.Length; ++i)
{
MyObject item = array[i];
MyObjectAction(item, writer);
}
Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
}
}
输出:
数组循环计数: 3102911316
需要注意的是,Resharper默认提供了一种重构建议,将上述for
语句更改为foreach
语句。这并不意味着这是正确的,但基础是减少代码中的技术债务。
TL;DR
除非在您的情况下测试显示您有真正的瓶颈(并且您必须拥有大量项目才能产生影响),否则您真的不应该关心这些事情的性能。
通常情况下,您应该选择最可维护的方法,此时Method 1(foreach
)是正确的选择。
System.Diagnostics.StopWatch
是您的好帮手。 - NolonarList<MyObject>
更改为IEnumerable<MyObject>
并使用foreach
,或者如果您根本不需要索引。我必须承认,在过去的10年中,我从未使用过List.ForEach
,除了在 SO 上回答问题之外。 - Tim Schmelter