.NET,你忘记了么?(三续)——重新理解List<T>
在上文中,《.NET,你忘记了么(三)——关于Array和List的使用》中,我对List<T>的理解大错特错,在成文前,首先做下自我批评,然后也对造成的不良影响表示道歉。
周四面试的惨败,让我的心情着实糟糕了两三天。痛定思痛,决心回家继续苦读。
首先开始的就是对List的重新认知。在这里,让我们先从构造方法来重新认识List<T>的本质,先来看下上文中我所粘出的代码:
List<int> list = new List<int>(); for (int i = 0; i < 10; i++) { list.Add(i); } Random r = new Random(); for (int j = 0; j < 100; j++) { int temp; int x1 = r.Next(10); int x2 = r.Next(10); temp = list[x1]; list[x1] = list[x2]; list[x2] = temp; }在上文中,我对这个List大批特批,现在,我们来重新看下这个List的构造:public List() { this._items = List<T>._emptyArray; }先来看无参的构造方法,当无参的时候,.NET Framework其实是用一个_emptyArray来初始化了List中的items集合。那么_emptyArray又是什么呢?我们继续向下看:
private static T[] _emptyArray;
恩,他是一个静态字段,然后我们看下List<T>的静态构造方法:
static List() { List<T>._emptyArray = new T[0]; }我们看到,_emptyArray其实是一个T类型的数组,个数为0。那么也就是说,当我们执行0参数的构造方法时,系统是把items集合给赋值为了一个T类型的个数为0的数组。
那么items又是什么?我们继续向下看:
public void Add(T item) { if (this._size == this._items.Length) { this.EnsureCapacity(this._size + 1); } this._items[this._size++] = item; this._version++; }这是List<T>中一个Add(T item)方法,但是我们可以从方法中敲出些端倪来。
在这里,我并不是想解释这个方法的原理,只是想说,在List中,其实维护这一个items,然后很多操作,是基于items的操作。
恩,所以在上文中,List<int> list=new List<int>();和Array a=new int[10]();的操作其实差别并不大。
我们肯定还记得在《Effective C#》中有这样一条规则,就是说:在初始化List之前最好对List初始化大小。
让我们从源码中来找到这一条规则的答案。
private void EnsureCapacity(int min) { if (this._items.Length < min) { int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2); if (num < min) { num = min; } this.Capacity = num; } }我们来看,在这个方法体中,List会新建一个数组,然后把数组的长度设置为原来的二倍(如果原有的数组长度为0,那就默认将数组的长度设置为4)。
因此,这种,让List的方法自己去调用EnsureCapacity(int min)方法,不仅浪费了构造数组的时间,还浪费了大量的空间(因为将原有的数组空间扩充了二倍)。
因此,请记得:在初始化List之前最好指定List的大小。
为了证明上述的观点,我们再来随便看一些代码:
public int IndexOf(T item) { return Array.IndexOf<T>(this._items, item, 0, this._size); }
public int FindIndex(int startIndex, int count, Predicate<T> match) { if (startIndex > this._size) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_Index); } if ((count < 0) || (startIndex > (this._size - count))) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count); } if (match == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); } int num = startIndex + count; for (int i = startIndex; i < num; i++) { if (match(this._items[i])) { return i; } } return -1; }由上面的代码,我想证明的是:其实List<T>不过是对Array的进一步封装,说得再直接点,我愿意理解List<T>为Array的可扩充版本,然后扩展了一些方法;
那关于Array和List的选择,我重新做一个说明:
List是基于Array存在的,因此,在创建一个List对象时,需要耗费比Array相对更多的时间,以及更大的空间,因为List除了初始化内部的items外还需要初始化一些其他的属性。而且在方法调用时,这点我没有证实,只是一个猜测,List需要的是再去调用Array的相关方法,因此也许会存在方法调用的时间消耗问题。
因此,我的建议是:
如果初始化时确定大小,那么就使用Array。
如果初始化时不确定大小,那么就使用List。当然,其实完全可以自己去实现List中的数组扩充功能的,也许会更棒,因为我们没有必要去将Array每次都扩充为原来的二倍。
另外,非主流程序员的补充,Array相对于List还有个优势就是:多维数组比List的嵌套更容易理解,也就是说int[][](或者是int[,])要强于List
>,也就说在类型确定且多维的情况下,用Array要优于List。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· [AI/GPT/综述] AI Agent的设计模式综述
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!