10

c#中容易被忽视的foreach - micDavid

 2 years ago
source link: https://www.cnblogs.com/wangqiang3311/p/16690138.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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的性能,先上结果:

110779-20220913172748677-930355068.png

 说明下,最后一个是对象调用Foreach方法。数据反映的是随着数据规模下降,看运行时间有什么变化。从1亿次循环到1万次循环,耗时从几百毫秒到1毫秒以内。从图上,明显能看出性能差异,是从千万级别开始,for的性能最好,其次是对象的Foreach方法,最后是foreach。

for和foreach的性能差异,我们尚且能理解,但是对象的Foreach和直接foreach差异从何而来?我冥思苦想,百思不得其解。我试图从内存分配和垃圾回收的机制方向去理解,但是没有突破。我想着,直接foreach耗时,是不是因为,它多执行了什么东西,比如说多分配了一些变量,比如说,内存中这么大数据量,垃圾回收机制,不可能无动于衷,是不是垃圾回收机制导致的程序变慢,进而影响了性能。

110779-20220913173942227-1529497809.png

 我在循环完后,强行执行了一次GC,才释放了13.671875k,说明循环中,执行GC也没有什么意义,回收不了垃圾,但是如果循环中,频繁执行GC,确实会导致程序没法好好地运行。垃圾回收机制,会把不再引用的对象释放,而整个循环过程中,对象都在List中,所以GC应该不会运行。

那亲爱的程序员朋友,你觉得对象的Foreach方法和直接Foreach的性能差异,是怎么产生的呢,欢迎讨论,我把源码贴出来。

放到Main方法里:

最后注意一点的是,foreach循环里面,不能随便添加或者删除元素,如果允许的话,程序将很难控制,而且非常容易出错,所以微软不允许这么干。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK