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( ));