很久很久以前,就聽說,for和foreach是不一樣的(不僅僅是文法),在網上也看到很很多說明的文章。
但從自己寫的代碼中來看,很難看出區別在那,因為大多數時候,都是用for或者foreach
對一個數組結構的類進行遍曆操作。
某天突然想弄清楚這個問題,於是小小的分析了一下,看看下段代碼:
public void For()
{
string[] array = new string[]{"111","222","333"};
for(int i = 0; i < array.Length; i++)
{
Console.WriteLine(array[i]);
}
}
////Results :
111
222
333
public void ForeachOnArray()
{
string[] array = new string[]{"111","222","333"};
foreach(string s in array)
{
Console.WriteLine(s);
}
}
////Results :
111
222
333
一樣的輸入結果咯!來看看ILdasm告訴我們的:
For:
Foreach:
奇怪,雖然所調用的指令不不同,但也是差不多,都是根據length來對資料進行迴圈。(與網上說得有點不一樣哦)
(在這裡主要不同的是foreach裡自動對array進行index + 1的操作來迴圈,而for則是自己的代碼控制的。)
再加點代碼,在迴圈中試圖更改所操作的值:
array = new string[]{"AAA","BBB","CCC"};
///For() result:
111
BBB
CCC
///ForeachOnArray() result:
111
222
333
不一樣了把,看來在foreach內部的迴圈中對源的更改不是即使生效的!
如果試著更改當前操作的數組內的值:
For() :
array[i] = "changed"; //OK
ForeachOnArray() :
s = "Changed"; //編譯時間Error,提示: “Cannot assign to 's' because it is read-only”
如果改為:
ForeachOnArray() :
array[2] = "Changed"; //在Foreach()內部無效,跳出Foreach()迴圈時更改才生效。
恩,以上的區別是很顯而易見了,
結論:在Foreach(...)迴圈裡盡量不要更改操作的源,
在For(...)迴圈裡則無所謂(看起來For跟do.While.的迴圈更類似,核心僅僅是判斷)
但這就是全部的真實了嗎?
NO,來考慮下我們經常在DataRowCollection( 即 DataTable.DataRow )上做的迴圈--它可不是一個數組,
而根據MS的參考,能在foreach上做迴圈的只能是實現了IEnumerable介面的類. (事實上,System.Array也是實現了IEnumerable介面的)
恩,現在拋開數組,來做一個在IEnumerable上的迴圈,先編寫如下的實現了IEnumberable介面的類E:
public class E : IEnumerable
{
private InnerEnumerator inner;
public E(string[] array)
{
this.inner = new InnerEnumerator(array);
}
#region IEnumerable Members
public IEnumerator GetEnumerator()
{
return this.inner;
}
#endregion
private class InnerEnumerator : IEnumerator, IDisposable
{
private string[] s;
private int currentIndex;
public InnerEnumerator(string[] array)
{
this.s = array;
this.Reset();
}
#region IEnumerator Members
//Reset index to original
public void Reset()
{
this.currentIndex = s.Length - 1;
}
//Get Current object inner
public object Current
{
get
{
object o = this.s[this.currentIndex];
this.currentIndex--;
return o;
}
}
//Is there has any other object in the array?
public bool MoveNext()
{
if(this.currentIndex < 0)
{
return false;
}
return true;
}
#endregion
#region IDisposable Members
//Dispose Here
public void Dispose()
{
Console.WriteLine("Dispose here !");
}
#endregion
}
}
接下來,拿這個類做測試,在上面做迴圈看看:(過程與For()和ForeachOnArray()大致一樣)
public void ForeachOnIEnumerable()
{
string[] array = new string[]{"111","222","333"};
E e = new E(array);
foreach(string s in e)
{
Console.WriteLine(s);
}
}
//result:
333
222
111
Dispose here !
差異出現了,這次是按照倒序的方式,而且看樣子還是自動調用了Dispose方法!
(NOTE : 我們可沒有自己編寫調用Dispose()方法的代碼!)
還是老規矩,用ILdasm看看實際執行的IL代碼:
果然,多了很多不一樣的東東
try
{
}
finally
{
}
塊
分析:
順序:
1.調用e.GetEnumerator()方法,擷取一個Enumerator執行個體
等同於代碼:
IEnumerator ienumerator = e.GetEnumertor();
2.在這個Enumerator執行個體上調用GetCurrent()方法,將擷取到的object 通過 castclass 指命
轉換為我們在代碼中定義的類型string
即等同與代碼:
string s = (string)ienumerator.GetCurrent();
(NOTE : 如果失敗,會拋出InvalidCastException異常,表明轉換失敗,類型不符)
(即:如果我們這裡改為:foreach(int s in array),就會發生運行時的錯誤,拋出InvalidCastException異常,
這就是castclass關鍵字的作用)
3.對這個GetCurrent()所得的object做自己想要得處理
在這裡就是我們自己編寫的代碼:
Console.WriteLine(s);
4.在擷取得IEnumerator上調用MoveNext(),如果true,則進入下次迴圈(跳轉到2),如果為false,則跳出迴圈
等同於代碼:
if(ienumerator.MoveNext())
{
goto step-2;
}
else
{
goto finally;
}
finally塊代碼:
1.判斷前面擷取的ienumerator是否實現了IDisposable介面,true則繼續,false則break;
等同樣代碼:
if(!(ienumerator is IDispose))
{
break; //over whole
}
2.調用IDisposable上的Dispose()方法(這就是為什麼輸入最後有一句"Dispose here !"了)
因為我們寫在Dispose()中的方法在這裡被自動調用了!!!
等同於代碼:
ienumerator.Dispose()
分析:
。。。
這與前面在Array上做的迴圈(ForeachOnArray())可大不一樣,是兩段完全不同的代碼!
綜合上面,得出如下結論:
1.for迴圈並不依賴於數組或其他形式的組式資料結構,只是簡單的
在調用了代碼後,進行一個判斷,判斷是否要繼續。
(非常類似於do..while和while迴圈--在這裡不作具體分析了^_^~~)
2.foreach迴圈如果作用在一個基於System.Array的類型之上的數組的話,編譯器會自動最佳化成與for迴圈非常類似
的代碼,只是調用的指命有細微的差別,並且檢查(包括編譯階段和運行時)會比for嚴格的多
3.foreach迴圈作用在一個非System.Array類型上(且一定要是實現了IEnumerable介面的類),會先調用
IEnumerable.GetEnumerator()方法擷取一個Enumertor執行個體,再在擷取的Enumertor執行個體上調用
GetCurrent()和MoveNext()方法,最後判斷如果Enumertor執行個體如果實現了IDispose介面,就自動調用
IDispose.Dispose()方法!
那麼我們應該分別在那些地方用for和foreach捏
建議:
1.在有對所迴圈的本體(System.Array)做賦值操作時,盡量不要用Foreach()。
2.foreach比for更靈活。(可在MoveNext()和GetCurrent()裡編寫自己的代碼).
自己編寫的類如果實現了IEnumerable介面的話,就可以用foreach迴圈了,而不管內部是否有一個真實的數組,
並且可以自訂迴圈的規則。
3.從OO的原則看,foreach迴圈更適於多數情況的使用
(事實上,foreach的實現是典型的Iterator模式,下面有簡單的描述它的好處)
想用統一的調用迴圈介面時,foreach是最佳的選擇
(MS有很多類就是這樣的,例如前面提到的DataRowCollection.)
補充說明:
/////////////////////////////////////////////////////////////////////////////////////////////
isinit:
如果再做進一步的實驗的話,可以發現,其實這裡的isinit就是C#的is關鍵字(而as關鍵字的核心也是isinit這個作業碼)
擷取反過來說,is,as關鍵字在IL代碼裡表現為這個isinit.
isinst不會觸發異常,只是判斷類型是否相容。
/////////////////////////////////////////////////////////////////////////////////////////////
castclass:
castclass用於強制轉換:
castclass指命會引發如下一樣:
InvalidCastException - Specified cast in not valid
而如果Foreach的作用在一個string[]類型的數組( 例如上面的代碼改為 : foreach(int s in array) )
這種錯誤在編譯時間就能檢測出來,提示:Cannot convert type 'string' to 'int'
/////////////////////////////////////////////////////////////////////////////////////////////
關於Iterator模式:
Iterator模式是用於遍曆集合類的標準存取方法。它可以把訪問邏輯從不同類型的集合類中抽象出來,
從而避免向用戶端暴露集合的內部結構。
特點:[引用]
"要確保遍曆過程順利完成,必須保證遍曆過程中不更改集合的內容,
因此,確保遍曆可靠的原則是只在一個線程中使用這個集合,或者在多線程中對遍曆代碼進行同步。"
這也能解釋為什麼C#.NET在Foreach上要做嚴格的使用限制,而For則沒有。
Iterator模式參考:
http://www.emagister.cn/cursos-java%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%9A%E6%B7%B1%E5%85%A5%E6%8E%A2%E8%AE%A8iterator%E6%A8%A1%E5%BC%8F-simcour-1636693.htm