有句俗語:百姓日用而不知。我們c#程序員很喜歡,也非常習(xí)慣地用foreach。今天呢,我就帶大家一起探索foreach,走,開始我們的旅程。
一、for語句用地好好的,為什么要提供一個(gè)foreach?
for (var i = 0; i < 10; i++)
{
//to do sth
}
foreach (var n in list)
{
//to do sth
}
|
首先,for循環(huán),需要知道循環(huán)的次數(shù),foreach不需要。其次,for循環(huán)在遍歷對(duì)象的時(shí)候,略顯麻煩,還需要通過下標(biāo)索引找到當(dāng)前對(duì)象,foreach不需要這么麻煩,顯得更優(yōu)雅。最后,for循環(huán)需要知道集合的細(xì)節(jié),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
怎樣的對(duì)象才能使用foreach呢?從微軟的文檔上看,實(shí)現(xiàn)了IEnumerable接口的對(duì)象,可以使用foreach,此接口只定義了一個(gè)方法:public System.Collections.IEnumerator GetEnumerator (); 有意思的是,它返回了一個(gè)IEnumerator接口,再看看這個(gè)接口:
有一個(gè)屬性:Current兩個(gè)方法MoveNext()、Reset(),現(xiàn)在我們回過頭來看看生成的IL代碼,真相大白。foreach只不過是個(gè)好吃的語法糖而已,編譯器幫我們做好了一切。和直接寫foreach類似的用法還有一個(gè),就是對(duì)象的Foreach方法:
list.ForEach(n => { //to do sth });
那問題就來了,都是foreach,我該用哪個(gè)?
忍不住看看微軟的源碼:
internal void ForEach(Action<T> action)
{
foreach (T x in this)
{
action(x);
}
}
其實(shí),就是定義了一個(gè)委托,我們把想要做的事情定義好,它來執(zhí)行。這和直接使用foreach有何區(qū)別?我又忍不住好奇心,寫了一段代碼,比較了for和foreach的性能,先上結(jié)果:
說明下,最后一個(gè)是對(duì)象調(diào)用Foreach方法。數(shù)據(jù)反映的是隨著數(shù)據(jù)規(guī)模下降,看運(yùn)行時(shí)間有什么變化。從1億次循環(huán)到1萬次循環(huán),耗時(shí)從幾百毫秒到1毫秒以內(nèi)。從圖上,明顯能看出性能差異,是從千萬級(jí)別開始,for的性能最好,其次是對(duì)象的Foreach方法,最后是foreach。
for和foreach的性能差異,我們尚且能理解,但是對(duì)象的Foreach和直接foreach差異從何而來?我冥思苦想,百思不得其解。我試圖從內(nèi)存分配和垃圾回收的機(jī)制方向去理解,但是沒有突破。我想著,直接foreach耗時(shí),是不是因?yàn)椋鄨?zhí)行了什么東西,比如說多分配了一些變量,比如說,內(nèi)存中這么大數(shù)據(jù)量,垃圾回收機(jī)制,不可能無動(dòng)于衷,是不是垃圾回收機(jī)制導(dǎo)致的程序變慢,進(jìn)而影響了性能。
我在循環(huán)完后,強(qiáng)行執(zhí)行了一次GC,才釋放了13.671875k,說明循環(huán)中,執(zhí)行GC也沒有什么意義,回收不了垃圾,但是如果循環(huán)中,頻繁執(zhí)行GC,確實(shí)會(huì)導(dǎo)致程序沒法好好地運(yùn)行。垃圾回收機(jī)制,會(huì)把不再引用的對(duì)象釋放,而整個(gè)循環(huán)過程中,對(duì)象都在List中,所以GC應(yīng)該不會(huì)運(yùn)行。
那親愛的程序員朋友,你覺得對(duì)象的Foreach方法和直接Foreach的性能差異,是怎么產(chǎn)生的呢,歡迎討論,我把源碼貼出來。
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("當(dāng)前數(shù)據(jù)規(guī)模:" + 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();
最后注意一點(diǎn)的是,foreach循環(huán)里面,不能隨便添加或者刪除元素,如果允許的話,程序?qū)⒑茈y控制,而且非常容易出錯(cuò),所以微軟不允許這么干。
該文章在 2023/5/18 15:19:27 編輯過