程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則11:選擇foreach循環

Effective C#原則11:選擇foreach循環

編輯:關於C#

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

(譯注:注意數組與集合的區別。數組是一次性分配的連續內 存,集合是可以動態添加與修改的,一般用鏈表來實現。而對於C#裡所支持的鋸 齒數組,則是一種折衷的處理。)

foreach總能保證最好的代碼。你不用 操心哪種結構的循環有更高的效率:foreach和編譯器為你代勞了。

如果 你並不滿足於高效,例如還要有語言的交互。這個世界上有些人(是的,正是他 們在使用其它的編程語言)堅定不移的認為數組的索引是從1開始的,而不是0。 不管我們如何努力,我們也無法破除他們的這種習慣。.Net開發組已經嘗試過。 為此你不得不在C#這樣寫初始化代碼,那就是數組從某個非0數值開始的。

// Create a single dimension array.
// Its range is [ 1 .. 5 ]
Array test = Array.CreateInstance( typeof( int ),
new int[ ]{ 5 }, new int[ ]{ 1 });

這段代碼應該足夠讓所 有人感到畏懼了(譯注:對我而言,確實有一點)。但有些人就是很頑固,無認你 如何努力,他們會從1開始計數。很幸運,這是那些問題當中的一個,而你可以 讓編譯器來“欺騙”。用foreach來對test數組進行迭代:

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

foreach語句知道如何檢測數組的上下限,所以你應該這樣做 ,而且這和for循環的速度是一樣的,也不用管某人是采用那個做為下界。

對於多維數組,foreach給了你同樣的好處。假設你正在創建一個棋盤。 你將會這樣寫兩段代碼:

private Square[,] _theBoard = new Square[ 8, 8 ];
// elsewhere in code:
for ( int i = 0; i < _theBoard.GetLength( 0 ); i++ )
 for( int j = 0; j < _theBoard.GetLength( 1 ); j++ )
  _theBoard[ i, j ].PaintSquare( );

取而代之的是,你可以這樣簡單的畫這個棋 盤:

foreach( Square sq in _theBoard )
  sq.PaintSquare( );

(譯注:本人不贊成這樣的方法。它隱藏了 數組的行與列的邏輯關系。循環是以行優先的,如果你要的不是這個順序,那麼 這種循環並不好。)

foreach語句生成恰當的代碼來迭代數組裡所有維數 的數據。如果將來你要創建一個3D的棋盤,foreach循環還是一樣的工作,而另 一個循環則要做這樣的修改:

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

(譯注:這樣看上去雖然代碼很多,但我覺得 ,只要是程序員都可以一眼看出這是個三維數組的循環,但是對於foreach,我 看沒人一眼可以看出來它在做什麼! 個人理解。當然,這要看你怎樣認識,這當 然可以說是foreach的一個優點。)

事實上,foreach循環還可以在每個維 的下限不同的多維數組上工作(譯注:也就是鋸齒數組)。 我不想寫這樣的代碼 ,即使是為了做例示。但當某人在某時寫了這樣的集合時,foreach可以勝任。

foreach也給了你很大的伸縮性,當某時你發現須要修改數組裡底層的數 據結構時,它可以盡可能多的保證代碼不做修改。我們從一個簡單的數組來討論 這個問題:

int [] foo = new int[100];

假設後 來某些時候,你發現它不具備數組類(array class)的一些功能,而你又正好要 這些功能。你可能簡單把一個數組修改為ArrayList:

// Set the initial size:
ArrayList foo = new ArrayList( 100 );

任何用for循環的代碼被破壞:

int sum = 0;
for ( int index = 0;
 // won't compile: ArrayList uses Count, not Length
 index < foo.Length;
 index++ )
 // won't compile: foo[ index ] is object, not int.
 sum += foo[ index ];

然而,foreach循環可以根據所操作的對象不同,而自 動編譯成不同的代碼來轉化恰當的類型。什麼也不用改。還不只是對標准的數組 可以這樣,對於其它任何的集合類型也同樣可以用foreach.

如果你的集 合支持.Net環境下的規則,你的用戶就可以用foreach來迭代你的數據類型。為 了讓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接口,編譯器可以自動優 化代碼為finally塊。但對你而言,明白這一點很重要,無論如何,foreach生成 了正確的代碼。

foreach是一個應用廣泛的語句。它為數組的上下限自成 正確的代碼,迭代多維數組,強制轉化為恰當的類型(使用最有效的結構),還有 ,這是最重要的,生成最有效的循環結構。這是迭代集合最有效的方法。這樣,你寫出的代碼更持久(譯注:就是不會因為錯誤而改動太多的代碼),第一次寫代 碼的時候更簡潔。這對生產力是一個小的進步,隨著時間的推移會累加起來。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved