Effective C# Item 11: Prefer foreach Loops

      在C#中相比for或者while循环,foreach循环能为任意的集合生成最好的循环代码。它的定义基于.Net Framework中关于集合的接口,C#的编译器可以为不同类型的集合生成最合适的代码。当我们遍历集合时,应当使用foreach来代替其他循环结构。下面是三种循环的代码:

int[] foo = new int[100];
//循环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的代码从实质上就好像是下面这段代码:

//循环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接口。

IEnumerator it = foo.GetEnumerator();
while(it.MoveNext())
{
      
//unboxing
      int i = (int)it.Current;
      Console.WriteLine(i.ToString());
}

      现在foreach会生成类似于这样的代码来处理数组:

for(int index = 0; index < foo.Length; index++)
{
      Console.WriteLine(foo[index].ToString());
}

      foreach始终能生成最优化的代码。我们不需要记住对于哪个结构哪种循环最合适,foreach和编译器会帮助我们搞定一切。

      有时我们除了效率之外还有别的要求,例如语言之间的转换。有些使用其他语言的编程者对于C#中索引的起始是0而不是1这一点很不适应。不论怎么努力,我们都不能改变他们长年累月留下的习惯。我们可以通过下面的代码得到不是从0开始的索引。

//创建[1-5]的一维数组
Array test = Array.CreateInstance(typeof(int)), new int[] {5}new Int[] {1});

      看了这段让人头疼的代码,大部分人都会接受数组索引从0开始的事实。但是还是有一些顽固分子,一定要使用从1开始的索引。幸运的是,我们可以通过编译器解决这些问题,使用foreach来编译数组:

foreach(int j in test)
{
      Console.WriteLine(j);
}

      foreach很清楚如何检查数组的上下界,因此我们不必担心有些人使用非0的数组下界,而且效率上不会有损失。另外循环变量是只读的(对于值类型),我们不能通过修改foreach循环变量来替换集合中的对象。另外当集合中的对象与循环变量的对象类型转换错误的时候,foreach会抛出异常。

      在操作多维数组的时候foreach会非常简单。假设我们要创建一个棋盘模型,使用二维数组来存储。我们需要写内外两个for循环来进行遍历,但是如果使用foreach,我们可以很简单的这样写

private Square[,] _theBoard = new Square[88];
foreach(Square sq in _theBoard)
{
      sq.PaintSquare();
}

      foreach会为不同的遍历对象生成合适的代码。有朝一日如果我们希望把棋盘改成3D的,foreach遍历还可以工作,但是其他的循环就需要修改代码了。而且对于多维且下界不同的数组,foreach都可以工作的很好。

      foreach给我们提供了更大的灵活性。当我们将存储数据的结构从数组改变为其他数据结构的时候,foreach遍历的方法几乎不需要修改。我们先来看一下这个简单的数组:

int[] foo = new int[100];

      假设后来我们发现这个地方需要使用ArrayList。我们只需改变foo的声明,对于foreach的遍历不需要改变。

ArrayList foo = new ArrayList(100);

      但是对于使用其他方式循环的代码就被破坏了,必须从新修改:

int sum = 0;
//错误  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循环就相当于下面的代码:

IEnumerator it = foo.GetEnumerator() as IEnumerator;
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,测试代码如下:

using System;

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可能并未如书中所说在循环中进行越界检验,因此会稍快一些。

      回到目录
 

 

posted on 2006-09-25 08:50  aiya  阅读(1116)  评论(4编辑  收藏  举报