深入探讨C#foreach语句

C# foreach语句不仅仅只是do...while或者for循环语句的一个变体。它会为我们的集合产生***的遍历代码。实际上,foreach语句的定义和.NET框架中的集合接口密切相关。对于一些特殊的集合类型,C#编译器会产生具有***效率的代码。遍历集合时,我们应该使用C# foreach语句,而非其他的循环构造。例如,对于下面三种循环:

创新互联公司专注于企业成都全网营销推广、网站重做改版、丰宁网站定制设计、自适应品牌网站建设、html5商城网站建设、集团公司官网建设、成都外贸网站建设公司、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为丰宁等各大城市提供网站开发制作服务。

 
 
 
  1. int [] foo = new int[100];  
  2.  
  3. // 循环1:  
  4.  
  5. foreach ( int i in foo)  
  6.  
  7. Console.WriteLine( i.ToString( ));  
  8.  
  9. // 循环2:  
  10.  
  11. for ( int index = 0;  
  12.  
  13. index < foo.Length;  
  14.  
  15. index++ )  
  16.  
  17. Console.WriteLine( foo[index].ToString( ));  
  18.  
  19. // 循环3:  
  20.  
  21. int len = foo.Length;  
  22.  
  23. for ( int index = 0;  
  24.  
  25. index < len;  
  26.  
  27. index++ )  
  28.  
  29. Console.WriteLine( foo[index].ToString( ));  

对于当前和将来的C#编译器(版本1.1及其以上版本),第1个循环产生的代码***,而且需要键入的字符也最少,因此程序员的开发效率也比较高。(不过在C# 1.0编译器下,第1个循环产生的代码效率较慢,第2个循环产生的代码效率***。)大多数C和C++程序员认为效率***的第3循环,反而是最坏的选择。通过将Length变量放到循环之外,我们实际上阻碍了JIT编译器移除循环中的范围检查。

C#代码运行在一个安全、托管的环境中。每一个内存位置都会被检查,包括数组索引。事实上,第3个循环所产生的代码和如下的代码等效:

 
 
 
  1. // 循环3, 和编译器产生的代码等效:  
  2.  
  3. int len = foo.Length;  
  4.  
  5. for ( int index = 0;  
  6.  
  7. index < len;  
  8.  
  9. index++ )  
  10.  
  11. {  
  12.  
  13. if ( index < foo.Length )  
  14.  
  15. Console.WriteLine( foo[index].ToString( ));  
  16.  
  17. else 
  18.  
  19. throw new IndexOutOfRangeException( );  
  20.  
  21. }  

JIT和C#编译器并不“喜欢”我们用这种方式来帮助它们。将Length属性放到循环之外只会让JIT编译器做更多的工作,产生的代码也更慢。CLR会确保我们写的代码不会滥用变量拥有的内存。CLR会在访问每一个特定数组元素之前,产生一个数组界限(并非上面的len变量)测试。如果我们像上面那样写代码,每一个数组界限测试会被执行两次。

在循环的每一次迭代中,我们都要对数组索引做两次检查。第1个循环和第2个循环更快的理由在于C#编译器和JIT编译器可以确保循环中的数组界限是安全的。只要循环变量不是数组的Length属性,每一次迭代时都会执行数组界限检查。

对于1.0版本的C#编译器,在数组上使用foreach语句产生的代码比较慢的原因在于装箱操作(有关装箱的详细讨论,参见条款17)。在.NET中,数组是类型安全的。1.1版本之后的C#编译器会为数组与其他集合产生不同的IL。在1.0版本的编译器产生的代码中,在数组上使用foreach语句实际上是通过IEnumerator接口来遍历数组,而这会导致装箱与拆箱操作:

 
 
 
  1. IEnumerator it = foo.GetEnumerator( );  
  2.  
  3. while( it.MoveNext( ))  
  4.  
  5. {  
  6.  
  7. int i = (int) it.Current; // 这里将出现装箱和拆箱。  
  8.  
  9. Console.WriteLine( i.ToString( ) );  
  10.  
  11. }  

相反,对于1.1版本之后的C#编译器,在数组上使用foreach语句将产生类似如下的构造:

 
 
 
  1. for ( int index = 0;  
  2.  
  3. index < foo.Length;  
  4.  
  5. index++ )  
  6.  
  7. Console.WriteLine( foo[index].ToString( ));  

由于foreach语句总会产生***的代码,所以我们不必刻意去记忆哪种构造会产生***效的循环构造——foreach和编译器会为我们做这些工作。

如果效率还不能说服大家,那么来看看语言互操作的情况。总有一些人(其中的大多数人都有使用其他一些编程语言的经验)坚定地认为数组的起始索引变量应该从1(而非0)开始。不管我们怎么费力地说服他们,都无法改变他们的这个习惯。.NET开发组在这个问题上已经尽力了。我们可以在C#语言中用如下的初始化方式,来获得一个起始索引不为0的数组:

 
 
 
  1. // 创建一个一维数组,范围为 [ 1 .. 5 ]。  
  2.  
  3. Array test = Array.CreateInstance( typeof( int ),  
  4.  
  5. new int[ ]{ 5 }, new int[ ]{ 1 });  

很多人面对这样的代码可能会退缩,转而使用起始索引为0的数组。但是总有一些人对此比较顽固。不管你怎么努力,这些人都会坚持从1开始索引数组。幸运地是,在这个问题上我们可以使用foreach语句来蒙混编译器:

 
 
 
  1. foreach( int j in test )  
  2.  
  3. Console.WriteLine ( j );  

这里的foreach语句知道如何获得数组的上下界,因此就不必烦劳我们——而且其效率和我们手写的for循环一样快,不管其他人采用的数组下界是多少,我们使用这种做法都可以正常工作。

另外,C# foreach语句还可以为我们带来其他好处。其中的循环变量是只读的——也就是说我们不能替换foreach语句中的集合对象。而且还存在一个显式强制转型。如果集合中保存的对象类型不正确,迭代语句将抛出一个异常。

对于多维数组,foreach语句也有类似的好处。假设我们要创建一个棋盘,我们可能会编写如下的两段代码:

 
 
 
  1. private Square[,] _theBoard = new Square[ 8, 8 ];  
  2.  
  3. // 另外地方的代码:  
  4.  
  5. for ( int i = 0; i < _theBoard.GetLength( 0 ); i++ )  
  6.  
  7. for( int j = 0; j < _theBoard.GetLength( 1 ); j++ )  
  8.  
  9. _theBoard[ i, j ].PaintSquare( );  

使用foreach语句,我们可以将上面的遍历代码做如下的简化:

 
 
 
  1. foreach( Square sq in _theBoard )  
  2.  
  3. sq.PaintSquare( );  

不管数组的维数是多少,foreach语句都会产生正确的遍历代码。如果我们之后又要做一个3D棋盘,上面的foreach循环仍然会正常工作。而其他手写的循环代码就需要更改了:

 
 
 
  1. for ( int i = 0; i < _theBoard.GetLength( 0 ); i++ )  
  2.  
  3. for( int j = 0; j < _theBoard.GetLength( 1 ); j++ )  
  4.  
  5. for( int k = 0; k < _theBoard.GetLength( 2 ); k++ )  
  6.  
  7. _theBoard[ i, j, k ].PaintSquare( );  

事实上,对于在每一维上拥有不同下界的多维数组来讲,foreach循环也会正常工作。这里我就不再编写这样的示例代码了。如果有人使用那样的集合,我们要知道foreach语句也能处理它。

如果我们刚开始使用的是数组,后来又需要转向其他数据结构,foreach语句允许我们不用更改绝大多数代码,从而保持代码的灵活性。假设我们刚开始有如下一个简单的数组:

 
 
 
  1. int [] foo = new int[100]; 

但过了一段时间后,我们发现该数组无法方便地处理我们需要的某种功能。这时候,我们选择将数组更改为ArrayList:

 
 
 
  1. // 设置初始大小:  
  2.  
  3. ArrayList foo = new ArrayList( 100 );  

这样更改之后,任何手写的for循环代码都将遭到破坏:

 
 
 
  1. int sum = 0;  
  2.  
  3. for ( int index = 0;  
  4.  
  5. // 下面的代码将不能编译:ArrayList 使用Count,而非Length。  
  6.  
  7. index < foo.Length;  
  8.  
  9. index++ )  
  10.  
  11. //下面的代码将不能编译:foo[ index ] 是一个object,而非int。  
  12.  
  13. sum += foo[ index ];  

而使用foreach语句,它会编译为不同的代码,自动将每一个操作数强制转换为正确的类型。我们在代码上无需做任何更改。事实上,使用foreach语句不仅可以更改为标准集合类型——任何集合类型都可以使用foreach。

如果我们支持.NET环境为集合所定义的规则,用户便可以使用foreach来遍历我们的类型成员。要让foreach语句将一个类看做集合类型,该类必须拥有一些属性。总共有3种方式可以使一个类成为集合类:类型具备一个公有的GetEnumerator()方法;类型显式实现了IEnumerable接口;类型实现了IEnumerator接口。

***,C# foreach语句还会为我们在资源管理方面带来额外的好处。IEnumerable接口只包含一个方法:GetEnumerator()。在一个支持IEnumerable接口的类型上使用foreach语句会产生类似如下的代码(会有一些优化):

 
 
 
  1. IEnumerator it = foo.GetEnumerator( ) as IEnumerator;  
  2.  
  3. using ( IDisposable disp = it as IDisposable )  
  4.  
  5. {  
  6.  
  7. while ( it.MoveNext( ))  
  8.  
  9. {  
  10.  
  11. int elem = ( int ) it.Current;  
  12.  
  13. sum += elem;  
  14.  
  15. }  
  16.  
  17. }  

如果编译器可以确定类型对IDisposable接口的实现情况,那么它就会自动优化finally块中的语句。

综上所述,foreach是一个非常有用的语句。它会使用***效的构造为“数组的上下界索引”、“多维数组遍历”和“操作数转型”产生正确的代码,并且产生的是***效率的循环结构。它是遍历集合的***方式。使用它,我们编写的代码将比较“经久耐用”,而且在刚开始编写的时候也比较简单。使用foreach为我们带来的开发效率提升可能很少,但是随着时间的推移,它的效益会不断增长。

C# foreach语句的深入了解的内容就向你介绍到这里,希望对你了解和学习C# foreach语句有所帮助。

网页标题:深入探讨C#foreach语句
链接URL:http://www.mswzjz.com/qtweb/news38/172738.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联