本文共 3157 字,大约阅读时间需要 10 分钟。
接触过C#循环的朋友,想来对foreach应该不会陌生,相比一般的for循环方式,foreach显得更加优雅简洁,Unity支持C#脚本,平日使用中数组列表什么的自然也会遇到不少,想来foreach定然大有用武之地呀!
可惜网上大家的共识却是:不要用foreach!
WTF?
原因其实也简单,就是为了避免,因为foreach会“偷偷”申请内存,使用过度的话自然会引发系统的垃圾收集!
有鉴于此,建议大家平日尽量限制使用foreach,转而使用for之类的循环控制语法,尤其注意一下Update(或者说频繁调用的函数)中的foreach使用,不小心的话确实会导致频繁GC~
OK,基础知识普及完毕,接下来让我们再细致看下(基于Unity5.3.3f1):
1. foreach真的会申请内存吗?申请多少内存?(或者叫GC Alloc)
简单写个测试,Profiler一下就明了了~
using UnityEngine;using System.Collections;using System.Collections.Generic;public class ForeachTest : MonoBehaviour { int[] m_array = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; ArrayList m_arrayList = new ArrayList { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; List m_list = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; void Update() { ForeachList(); } void ForeachArray() { // foreach array foreach (var element in m_array) { Func(element); } } void ForeachArrayList() { // foreach array list foreach (var element in m_arrayList) { Func((int)element); } } void ForeachList() { // foreach list foreach (var element in m_list) { Func(element); } } void Func(int element) { } }
可以看到,foreach一个List确实会产生内存申请,大小为40字节~
2. 为什么foreach会申请内存呢?
说到这个问题,我们便需要进一步的认识一下foreach了,相比传统的for,foreach其实是C#的一种,还拿上面的测试程序举例,foreach一个List最后会被C#翻译为大概下面这种形式:
using (List .Enumerator enumerator = list.GetEnumerator()){ while (enumerator.MoveNext()) { int current = enumerator.Current; this.Func(current); }}
初看上去似乎没有什么申请内存的地方,但是注意到这里using的使用,其最后会通过IDisposable接口调用Dispose,但是由于List的Enumerator是个值类型,转换为IDisposable接口会导致,继而便引发了内存申请~
使用看下生成的IL便更加一目了然了:
3. foreach List会触发GC Alloc,那么其他类型的列表类型是不是一样呢?
首先看下原生数组:
void ForeachArray(){ // foreach array foreach (var element in m_array) { Func(element); }}
竟然没有产生GC Alloc?看下转换后的代码:
// ForeachTestprivate void ForeachArray(){ int[] array = this.m_array; for (int i = 0; i < array.Length; i++) { int element = array[i]; this.Func(element); }}
看来C#对于原生数组的foreach形式做了优化,使用了传统的for来遍历数组,自然便不会申请额外的内存了~
再来试下ArrayList~
void ForeachArrayList(){ // foreach array list foreach (var element in m_arrayList) { Func((int)element); }}
看来同List一样,也会产生40字节的GC Alloc,同样的看下转换后的代码:
// ForeachTestprivate void ForeachArrayList(){ using (IEnumerator enumerator = this.m_arrayList.GetEnumerator()) { while (enumerator.MoveNext()) { object current = enumerator.get_Current(); this.Func((int)current); } }}
形式上与foreach List如出一辙,但是值得指出的是,这里产生内存申请的地方与foreach List是不同的,foreach List如上面所说,是由于装箱操作而引起的GC Alloc,但是foreach ArrayList则是由于GetEnumerator,因为ArrayList的Enumerator 是引用类型,创建时自然会在堆上分配(也就是产生了内存分配),后面虽然也会尝试转换为IDisposable接口来调用Dispose,但是因为是引用类型间的转换,并不会引发Box~
IL代码最能说明问题:
4. 真的不能再使用foreach了吗?
诚然,foreach会产生内存申请,但是相对而言GC Alloc的大小还是相对有限的(上面看到是40B),所以只要不是频繁调用,这点消耗还是能够接受的;再者,如果你使用的是原生数组,那么便不用担心了,随意使用foreach即可,因为就像上面看到的那样,foreach原生数组并不会产生GC Alloc;最后,其实新版的C#早已修复了foreach会产生额外内存申请的问题,只是由于Unity内含的Mono版本较早,没有修复该问题罢了,如果你想痛快的在Unity中使用foreach,可以看看和~
OK,没想简单的一个foreach也讲了这么多东西,其中的知识其实网上早已有了很多优秀的解释,知乎上的一篇相关想来应该是个不错的起点,有兴趣的朋友可以仔细看看~
好了,下次再见吧~