Effective C# Item 11: Prefer foreach Loops
在C#中相比for或者while循环,foreach循环能为任意的集合生成最好的循环代码。它的定义基于.Net Framework中关于集合的接口,C#的编译器可以为不同类型的集合生成最合适的代码。当我们遍历集合时,应当使用foreach来代替其他循环结构。下面是三种循环的代码:
//循环1
foreach(int i in foo)
{
Console.WriteLine(i.ToString());
}
//循环2
for(int index = 0; index < foo.Length; index++)
{
Console.WriteLine(foo[index].ToString());
}
//循环3
int len = foo.Length;
for(int index = 0; index < len; index++)
{
Console.WriteLine(foo[index].ToString());
}
对于目前的C#编译器来说(1.1或更高版本),循环1是最好的 (对于1.0版本来说循环1要慢很多,所以循环2是最好的)。循环3是一般C和C++编程人员的写法,看起来是高效的,其实效率是最差的。在循环外取得Length的值的做法使得JIT编译器不能将集合的边界检查从循环中移除。也就是说每次循环都要检查数组是否越界。(P.S. 我在测试的时候发现得到的结果同书中所说的不同,不知道是不是我的测试方法有问题。我把这部分的测试放到了这篇文章的最后,也请大家指点一下。3x)
C#代码要运行在安全可控的环境中,每一个内存地址,包括数组的索引都要被检查。循环3的代码从实质上就好像是下面这段代码:
int len = foo.Length;
for(int index = 0; index < len; index++)
{
if(index < foo.Length)
{
Console.WriteLine(foo[index].ToString());
}
else
{
throw new IndexOutOfRangeException();
}
}
这样的做法是不推荐的。我们将Length属性提到循环外却只是生成更慢的代码。我们在每使用一个数组成员的时候还需要先检查它是否越界。在循环1和循环2中,编译器可以确定循环的索引是安全的,这就是为什么它们比循环3会快一些的原因。
在老版本中foreach和数组操作速度慢的原因是因为boxing的缘故。由于数组是安全的,现在foreach为数组生成不同于其他集合的IL。在这样的IL中数组不再使用需要boxing和unboxing的IEnumerator接口。
while(it.MoveNext())
{
//unboxing
int i = (int)it.Current;
Console.WriteLine(i.ToString());
}
现在foreach会生成类似于这样的代码来处理数组:
{
Console.WriteLine(foo[index].ToString());
}
foreach始终能生成最优化的代码。我们不需要记住对于哪个结构哪种循环最合适,foreach和编译器会帮助我们搞定一切。
有时我们除了效率之外还有别的要求,例如语言之间的转换。有些使用其他语言的编程者对于C#中索引的起始是0而不是1这一点很不适应。不论怎么努力,我们都不能改变他们长年累月留下的习惯。我们可以通过下面的代码得到不是从0开始的索引。
Array test = Array.CreateInstance(typeof(int)), new int[] {5}, new Int[] {1});
看了这段让人头疼的代码,大部分人都会接受数组索引从0开始的事实。但是还是有一些顽固分子,一定要使用从1开始的索引。幸运的是,我们可以通过编译器解决这些问题,使用foreach来编译数组:
{
Console.WriteLine(j);
}
foreach很清楚如何检查数组的上下界,因此我们不必担心有些人使用非0的数组下界,而且效率上不会有损失。另外循环变量是只读的(对于值类型),我们不能通过修改foreach循环变量来替换集合中的对象。另外当集合中的对象与循环变量的对象类型转换错误的时候,foreach会抛出异常。
在操作多维数组的时候foreach会非常简单。假设我们要创建一个棋盘模型,使用二维数组来存储。我们需要写内外两个for循环
foreach(Square sq in _theBoard)
{
sq.PaintSquare();
}
foreach会为不同的遍历对象生成合适的代码。有朝一日如果我们希望把棋盘改成3D的,foreach遍历还可以工作,但是其他的循环就需要修改代码了。而且对于多维且下界不同的数组,foreach都可以工作的很好。
foreach给我们提供了更大的灵活性。当我们将存储数据的结构从数组改变为其他数据结构的时候,foreach遍历的方法几乎不需要修改。我们先来看一下这个简单的数组:
假设后来我们发现这个地方需要使用ArrayList。我们只需改变foo的声明,对于foreach的遍历不需要改变。
但是对于使用其他方式循环的代码就被破坏了,必须从新修改:
//错误 ArrayList使用Count返回长度,而不是数组的Length
for(int index = 0; index < foo.Length; index++)
{
//错误 foo[index]是object型
sum += foo[index];
}
foreach可以自动的将其转换为对应的类型并编译为不同的代码。而且不仅对于标准的集合类型,任何集合类型都可以使用foreach。
如果我们的类型满足.Net的集合类型的规则,那么我们就可以使用foreach来遍历其中的成员。通过GetEnumerator()方法返回的集合、实现了IEnumerable接口的集合类型和实现了IEnumerator接口的集合类型都可以使用foreach来进行遍历。
foreach的另一个好处在于对资源的消耗不大。它调用了IEnumerable接口中包含的GetEnumerator()方法。因此foreach循环就相当于下面的代码:
using(IDisposable disp = it as IDisposable)
{
while(it.MoveNext())
{
int elem = (int)it.Current;
sum += elem;
}
}
编译器会自动优化这段代码,并在结束后调用IDisposable释放资源。但是对于我们来说这并不重要。我们只要使用foreach就行了,因为它总是生成最合适的代码。
foreach的适用面很广。它可以应对不同上下界的数组、遍历多维数组、自动进行类型转换,最重要的是循环的效率高,是用来遍历的最佳选择。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
P.S. 关于三种循环的效率问题,测试结果与书中所写不符
我所用的测试是对数组进行遍历,.Net版本为1.1,测试代码如下:
class Class1
{
[System.Runtime.InteropServices.DllImport ("Kernel32.dll")]
static extern bool QueryPerformanceCounter(ref long count);
[System.Runtime.InteropServices.DllImport ("Kernel32.dll")]
static extern bool QueryPerformanceFrequency(ref long count);
[STAThread]
static void Main(string[] args)
{
long count = 0;
long count1 = 0;
long freq = 0;
double result = 0;
int tmp = 0;
int[] foo = new int[10000];
QueryPerformanceFrequency(ref freq);
QueryPerformanceCounter(ref count);
// int len = 10000; //循环3
//开始的时候没有这层循环,所得数据浮动很大,添加这层循环来使得结果更加平均
for (int i=0; i<500; i++)
{
//循环1
foreach(int u in foo)
{
tmp += u;
}
//循环2
// for(int j = 0; j < foo.Length; j++)
// {
// tmp += foo[j];
// }
//循环3
// for(int j = 0; j < len; j++)
// {
// tmp += foo[j];
// }
}
QueryPerformanceCounter(ref count1);
count = count1-count;
result = (double)(count)/(double)freq;
Console.WriteLine("耗时: {0} 秒", result);
Console.ReadLine();
}
}
测试结果如下:(所耗时为重复500次的累计值,单位为秒)
循环1千次 | 循环1万次 | 循环10万次 | |
循环1 | 0.0020 | 0.0215 | 0.2546 |
循环2 | 0.0021 | 0.0214 | 0.2436 |
循环3 | 0.0016 | 0.0161 | 0.1850 |
从结果上看循环1和循环2基本不分上下,而循环3要稍快一些,特别是在循环次数较多的情况下这点更为明显。循环1和循环2应当是同样的实现方法。循环3可能并未如书中所说在循环中进行越界检验,因此会稍快一些。
回到目录