有句俗語:百姓日用而不知。我們c#程式設計師很喜歡,也非常習慣地用foreach。今天呢,我就帶大家一起探索foreach,走,開始我們的旅程。
一、for語句用的好好的,為什麼要提供一個foreach?
for (var i = 0; i < 10; i++) { //to do sth } foreach (var n in list) { //to do sth }
首先,for迴圈,需要知道迴圈的次數,foreach不需要。其次,for迴圈在遍歷物件的時候,略顯麻煩,還需要通過下標索引找到當前物件,foreach不需要這麼麻煩,顯得更優雅。最後,for迴圈需要知道集合的細節,foreach不需要知道。
這一切的好處,得益於微軟的封裝,那我們看看foreach生成的IL程式碼:
IL_00a7: callvirt instance valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<!0>
class [System.Collections]System.Collections.Generic.List`1<int64>::GetEnumerator() .try { IL_00ae: br.s IL_00c9 IL_00b0: ldloca.s V_10 IL_00b2: call instance !0 valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<int64>::get_Current() IL_00cb: call instance bool valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<int64>::MoveNext() IL_00d0: brtrue.s IL_00b0 IL_00d2: leave.s IL_00e3 } // end .try finally { IL_00d6: constrained. valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<int64> IL_00dc: callvirt instance void [System.Runtime]System.IDisposable::Dispose() IL_00e1: nop IL_00e2: endfinally } // end handlers
怎樣的物件才能使用foreach呢?從微軟的檔案上看,實現了IEnumerable介面的物件,可以使用foreach,此介面只定義了一個方法:public System.Collections.IEnumerator GetEnumerator (); 有意思的是,它返回了一個IEnumerator介面,再看看這個介面:
有一個屬性:Current和兩個方法MoveNext()、Reset(),現在我們回過頭來看看生成的IL程式碼,真相大白。foreach只不過是個好吃的語法糖而已,編譯器幫我們做好了一切。和直接寫foreach類似的用法還有一個,就是物件的Foreach方法:
list.ForEach(n => { //to do sth });
那問題就來了,都是foreach,我該用哪個?忍不住看看微軟的原始碼:
internal void ForEach(Action<T> action) { foreach (T x in this) { action(x); } }
其實,就是定義了一個委託,我們把想要做的事情定義好,它來執行。這和直接使用foreach有何區別?我又忍不住好奇心,寫了一段程式碼,比較了for和foreach的效能,先上結果:
說明下,最後一個是物件呼叫Foreach方法。資料反映的是隨著資料規模下降,看執行時間有什麼變化。從1億次迴圈到1萬次迴圈,耗時從幾百毫秒到1毫秒以內。從圖上,明顯能看出效能差異,是從千萬級別開始,for的效能最好,其次是物件的Foreach方法,最後是foreach。
for和foreach的效能差異,我們尚且能理解,但是物件的Foreach和直接foreach差異從何而來?我冥思苦想,百思不得其解。我試圖從記憶體分配和垃圾回收的機制方向去理解,但是沒有突破。我想著,直接foreach耗時,是不是因為,它多執行了什麼東西,比如說多分配了一些變數,比如說,記憶體中這麼巨量資料量,垃圾回收機制,不可能無動於衷,是不是垃圾回收機制導致的程式變慢,進而影響了效能。
我在迴圈完後,強行執行了一次GC,才釋放了13.671875k,說明迴圈中,執行GC也沒有什麼意義,回收不了垃圾,但是如果迴圈中,頻繁執行GC,確實會導致程式沒法好好地執行。垃圾回收機制,會把不再參照的物件釋放,而整個迴圈過程中,物件都在List中,所以GC應該不會執行。
那親愛的程式設計師朋友,你覺得物件的Foreach方法和直接Foreach的效能差異,是怎麼產生的呢,歡迎討論,我把原始碼貼出來。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; namespace MyConsole.Test { public class ForeachTest { public static void Test(long num) { Console.WriteLine("當前資料規模:" + num); DateTime start = DateTime.Now; for (var i = 0; i < num; i++) { var t = (i + 1) * 100 + 1; } DateTime end = DateTime.Now; var costTime = end.Subtract(start).TotalMilliseconds; Console.WriteLine("for cost time:" + costTime + " ms"); List<long> list = new List<long>(); for (var i = 0; i < num; i++) { list.Add(i); } start = DateTime.Now; foreach (var n in list) { var t = (n + 1) * 100 + 1; } end = DateTime.Now; costTime = end.Subtract(start).TotalMilliseconds; Console.WriteLine("foreach cost time:" + costTime + " ms"); start = DateTime.Now; list.ForEach(n => { var t = (n + 1) * 100 + 1; }); end = DateTime.Now; costTime = end.Subtract(start).TotalMilliseconds; Console.WriteLine("obj foreach cost time:" + costTime + " ms"); Console.WriteLine("--------------------------------------------"); Console.WriteLine(""); } } }
放到Main方法裡:
long[] nums = { 100000000, 10000000, 1000000, 100000, 10000, }; foreach (int num in nums) { for (int i = 0; i < 5; i++) { ForeachTest.Test(num); } } Console.ReadLine();
最後注意一點的是,foreach迴圈裡面,不能隨便新增或者刪除元素,如果允許的話,程式將很難控制,而且非常容易出錯,所以微軟不允許這麼幹。