c#中容易被忽視的foreach

有句俗語:百姓日用而不知。我們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循環裏面,不能隨便添加或者刪除元素,如果允許的話,程序將很難控制,而且非常容易出錯,所以微軟不允許這麼幹。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章