C#的foreach语句是从do,while,或者for循环语句变化而来的,它相对要好 一些,它可以为你的任何集合产生最好的迭代代码。它的定义依懒于.Net框架里 的集合接口,并且编译器会为实际的集合生成最好的代码。当你在集合上做迭代 时,可用使用foreach来取代其它的循环结构。检查下面的三个循环:
int [] foo = new int[100];
// Loop 1:
foreach ( int i in foo)
Console.WriteLine( i.ToString( ));
// Loop 2:
for ( int index = 0; index < foo.Length; index++ )
Console.WriteLine( foo[index].ToString( ));
// Loop 3:
int len = foo.Length;
for ( int index = 0; index < len; index++ )
Console.WriteLine( foo[index].ToString( ));
对于 当前的C#编译器(版本1.1或者更高)而言,循环1是最好的。起码它的输入要少些 ,这会使你的个人开发效率提提升。(1.0的C#编译器对循环1而言要慢很多,所 以对于那个版本循环2是最好的。) 循环3,大多数C或者C++程序员会认为它是最 有效的,但它是最糟糕的。因为在循环外部取出了变量Length的值,从而阻碍了 JIT编译器将边界检测从循环中移出。
C#代码是安全的托管代码里运行的 。环境里的每一块内存,包括数据的索引,都是被监视的。稍微展开一下,循环 3的代码实际很像这样的:
// Loop 3, as generated by compiler:
int len = foo.Length;
for ( int index = 0; index < len; index++ )
{
if ( index < foo.Length )
Console.WriteLine( foo[index].ToString( ));
else
throw new IndexOutOfRangeException( );
}
C#的JIT编译 器跟你不一样,它试图帮你这样做了。你本想把Length属性提出到循环外面,却 使得编译做了更多的事情,从而也降低了速度。CLR要保证的内容之一就是:你 不能写出让变量访问不属于它自己内存的代码。在访问每一个实际的集合时,运 行时确保对每个集合的边界(不是len变量)做了检测。你把一个边界检测分成了 两个。
你还是要为循环的每一次迭代做数组做索引检测,而且是两次。 循环1和循环2要快一些的原因是因为,C#的JIT编译器可以验证数组的边界来确 保安全。任何循环变量不是数据的长度时,边界检测就会在每一次迭代中发生。 (译注:这里几次说到JIT编译器,它是指将IL代码编译成本地代码时的编译器, 而不是指将C#代码或者其它代码编译成IL代码时的编译器。其实我们可以用不安 全选项来迫使JIT不做这样的检测,从而使运行速度提高。)
原始的C#编 译器之所以对foreach以及数组产生很慢的代码,是因为涉及到了装箱。装箱会 在原则17中展开讨论。数组是安全的类型,现在的foreach可以为数组生成与其 它集合不同的IL代码。对于数组的这个版本,它不再使用IEnumerator接口,就 是这个接口须要装箱与拆箱。
IEnumerator it = foo.GetEnumerator( );
while( it.MoveNext( ))
{
int i = (int) it.Current; // box and unbox here.
Console.WriteLine( i.ToString( ) );
}
取而代之的是,foreach语句为数组生 成了这样的结构:
for ( int index = 0; index < foo.Length; index++ )
Console.WriteLine( foo[index].ToString( ));