自己动手,实现一种类似List<T>的数据结构(一)
前言
上一篇文章《Unity3D中常用的数据结构总结与分析》简单总结了一下小匹夫工作中经常遇到的一些数据结构。不过小匹夫一直有种观点,就是光说的热闹实际啥也不做真的没啥意思。光说不练假把式,那么这篇文章不如记录一下小匹夫自己动手实现一个有类似功能的数据结构的过程吧。
模仿List<T>
寻思半天,写代码是为了啥?不是为了写以致用嘛?那么小匹夫工作中用的最多的数据结构是啥?思来想去还就是List<T>了,而且平时使用的时候的确也觉得有自己定制的空间。作为一个类,重要的无非是它的名字,构造函数,属性和各种方法,因为小匹夫喜欢吃鸡蛋,再加上一个好朋友的喵叫蛋壳,所以咱们的新数据结构就叫做EggArray<T>好了,既然是要模仿List<T>,那么定好类名之后我们自然需要去参考一下List<T>的构造函数,属性和方法(列出的都是公有的)从而进一步来确定我们自己的类成员咯。不过呢,首先我们要先明确我们自己的EggArray<T>到底需要怎么实现,以及实现哪些功能,List<T>只是我们模仿的对象,如果实现的还都是List<T>自己的那一套,我们也就没有什么必要做现在的这些事情了。
EggArray<T>是什么
上一篇文章分析过,List<T>的内部其实也是一个Array,且是强类型的,所以我们的EggArray<T>也秉承这个特点,内部通过一个Array来实现,且需要声明类型。但是同时我们也看到List<T>继承和实现了很多接口,比如能实现foreach方法的IEnumerable接口等,而且值类型和引用类型通吃。这里为了EggArray<T>实现起来轻装简行,我们不继承List<T>继承的各种接口,同时我们的EggArray只服务于引用类型。(也是从方便和使用的角度考虑,毕竟值类型不能赋值null,引用类型可以赋值null这一点,作为一个博客的内容就没有必要去考虑服务值类型了)。那么小伙伴可能想问了,不继承那些接口,像最基本的foreach这种需求是不是匹夫混蛋你就不想实现了?NO,NO,俗话说得好,"车到山前必有路,听说委托也不错"。。。咳咳扯远了,其实也很简单,小匹夫上上篇文章《Unity3D中使用委托和事件(一)》介绍过的委托代理其实就可以用来实现EggArray<T>的foreach功能,甚至还有好多小匹夫自己定制的功能,比如Map,Filter,Without之类的。下面具体实现的时候小匹夫还会再扯。
EggArray<T>的成员
那么明确了大的方向,再经过小匹夫自己的定制,对List<T>的成员进行增减之后,我们的EggArray<T>类和它的成员(变量&&属性,构造函数,私有方法,公有方法,小匹夫定制方法(在下一篇中说))如下:
EggArray类
//EggArray类 public class EggArray<T> where T : class { }
属性&变量(暂定,下一篇还会根据情况扩充):
属性 |
说明 |
Capacity | EggArray的容量 |
Count | EggArray中的元素个数 |
items | T[],一个Array,因为上一篇文章说过List<T>的内部其实还是Array,所以内部我们也使用Array |
foreachHandler | 一个delegate,用来实现foreach的功能 |
//EggArray<T>的属性&&变量 private int capacity; private int count; private T[] items; public delegate void foreachHandler(T item); public int Count { get { return this.count; } } public int Capacity { get { return this.capacity; } }
构造函数:
构造函数 | 说明 |
EggArray() | 初始化 EggArray<T> 类的新实例,该实例为空并且具有默认初始容量。 |
EggArray(int32) | 初始化 EggArray<T> 类的新实例,该实例为空并且具有指定的初始容量。 |
//EggArray的构造函数,默认容量为8 public EggArray() : this(8) { } public EggArray(int capacity) { this.capacity = capacity; this.items = new T[capacity]; }
下面就是EggArray的各种方法了,上文小匹夫已经说过了,咱们这里只是参考List<T>列出来的一些公共方法,有一些List<T>烂大街的方法比如Add,Rmove这些公共方法肯定都是要实现的,可是一些私有方法咱们平时接触不到呀,甚至在List<T>也没有查到。那么小匹夫觉得很重要,也很能体现咱们EggArray<T>长度十分灵活特点的一个私有方法,应该就非那个能灵活改变数组长度的方法莫属了吧?我们称之为Resize()好了。
小匹夫还说过要自己定制一些平时会用到,但List<T>并没有现成方法的方法了。比如把EggArray<T>中的每个值映射到一个新的数组中的Map方法,遍历List中的每个值,返回包含所有通过predicate真值检测的元素值的Filter方法,或者是遍历List,以List中的元素的某个成员进行排序的indexBy方法,还有返回一个除去所有null值的Compact方法等等。下面就按照这3类不同的方法列出来我们的EggArray<T>中的方法。
私有方法
私有方法 | 说明 |
Resize | 当数组元素个数大于或等于数组的容量时,调用该方法进行扩容,会创建一个新的Array存放数据,“增长因子”为2 |
//当数组元素个数不小于数组容量时,需要扩容,增长因子growthFactor为2 private void Resize() { int capacity = this.capacity * growthFactor; if (this.count > capacity) { this.count = capacity; } T[] destinationArray = new T[capacity]; Array.Copy(this.items, destinationArray, this.count); this.items = destinationArray; this.capacity = capacity; }
公共方法(List<T>也有的)
公共方法 | 说明 |
Add | 将对象添加到 EggArray<T> 的结尾处。 |
AddRange | 将指定集合的元素添加到 EggArray<T> 的末尾。 |
Insert | 将元素插入 EggArray<T> 的指定索引处。 |
Contains | 确定某元素是否在 EggArray<T> 中。 |
Clear | 从 EggArray<T> 中移除所有元素。 |
ToArray | 将 EggArray<T> 的元素复制到新数组中。 |
Sort | 使用默认比较器对整个 EggArray<T> 中的元素进行排序。 |
Foreach | 对 EggArray<T> 的每个元素执行指定操作。 |
Remove | 从 EggArray<T> 中移除特定对象的第一个匹配项。 |
RemoveAt | 移除 EggArray<T> 的指定索引处的元素。 |
Find | 搜索与指定谓词所定义的条件相匹配的元素,并返回整个 EggArray<T> 中的第一个匹配元素。 |
IndexOf | 搜索指定的对象,并返回整个 EggArray<T> 中第一个匹配项的从零开始的索引。 |
///List<T>已有的功能 /// <summary> /// Add the specified item. /// </summary> /// <param name="item">Item.</param> public void Add(T item) { if (this.count >= this.capacity) { this.Resize(); } this.items[this.count++] = item; } /// <summary> /// Adds the range. /// </summary> /// <param name="collection">Collection.</param> public void AddRange(IEnumerable<T> collection) { if (collection != null) { foreach (T current in collection) { this.Add(current); } } } /// <summary> /// Insert the specified index and item. /// </summary> /// <param name="index">Index.</param> /// <param name="item">Item.</param> public void Insert(int index, T item) { if (this.count >= this.capacity) { this.Resize(); } this.count++; for (int i = this.count - 1; i > index; i--) { this.items[i] = this.items[i - 1]; } this.items[index] = item; } /// <summary> /// Contains the specified arg. /// </summary> /// <param name="arg">Argument.</param> public bool Contains(T arg) { for (int i = 0; i < this.count; i++) { if (this.items[i].Equals(arg)) { return true; } } return false; } /// <summary> /// Clear this instance. /// </summary> public void Clear() { if (this.count > 0) { for (int i = 0; i < this.count; i++) { this.items[i] = null; } this.count = 0; } } /// <summary> /// Tos the array. /// </summary> /// <param name="array">Array.</param> public void ToArray(T[] array) { if (array != null) { for (int i = 0; i < this.count; i++) { array[i] = this.items[i]; } } } /// <summary> /// Sort the specified comparer. /// </summary> /// <param name="comparer">Comparer.</param> public void Sort(IComparer<T> comparer) { Array.Sort<T>(this.items, 0, this.count, comparer); } /// <summary> /// Foreach the specified handler. /// </summary> /// <param name="handler">Handler.</param> public void Foreach(EggArray<T>.IterationHandler handler) { for (int i = 0; i < this.count; i++) { handler(this.items[i]); } } /// <summary> /// Remove the specified arg. /// </summary> /// <param name="arg">Argument.</param> public bool Remove(T arg) { for (int i = 0; i < this.count; i++) { if (this.items[i].Equals(arg)) { this.items[i] = null; this.Compact(); return true; } } return false; } /// <summary> /// Removes at index. /// </summary> /// <param name="index">Index.</param> public void RemoveAt(int index) { if (index < this.count) { this.items[index] = null; this.Compact(); } } /// <summary> /// Indexs the of. /// </summary> /// <returns>The of.</returns> /// <param name="arg">Argument.</param> public int IndexOf(T arg) { for (int i = 0; i < this.count; i++) { if (this.items[i].Equals(arg)) { return i; } } return -1; }
以上便是我们仿照List<T>的公共方法所要实现的我们自己的公共方法,但是看说明我们很快就能发现一个问题。啥嘞?对嘞,就是很多方法都有方向性。比如Add方法,是将新的对象添加到EggArray<T>的末尾,可是我想要加到最开始怎么办。又或者Find方法,返回第一个满足条件的元素,但是要是我想要找最后一个匹配的呢?类似的问题还存在于IndexOf,Remove等等。所以这就是我们定制我们自己方法的定制思路之一:为了拓展已有方法的适用范围。(关于两端操作,大家想到了什么吗?没错,就是LinkedList,但是LinkedList本质上是链表,而我们的内部实现其实是Array,所以只是借鉴一下LinkedList的功能而非实现方法。其实这里对insert方法的实现就能看出和EggArray内部同为Array的List<T>在处理中间插入新的元素是多蛋疼的一件事情)
但是我们回到List<T>的MSDN页面,看看罗列出来的公有方法,总觉得少了点什么。哎?最直观的,貌似没有Slice呀。或者是我想做一些有限的过滤功能以得到符合我们简单需求的新数组,哎?貌似也没有Filter之类的功能?其实我们还有好多需求。。。那么我们第二条定制思路就有了:为了实现List<T>没有实现而我们日常需要用到的功能。在继续下面的内容之前,还是要简单说明一下几个需要注意的点。
- Insert方法,上面已经说过了,处理元素插入时,数组是不如链表的。
- Contains、IndexOf等方法,这里需要说明一下,在这些方法中我使用了.equles来判断作为参数传入的元素是否与数组内的元素值相同。作为一个处理引用类型的数据结构,我还是要说明一下equles和==的区别,即equals是比较他们的值,而==相当于比较它们在堆中的位置!即==判断的是是否是同一个对象。为了严谨,下面还将引入用==进行比较确定元素身份的方法。
- Foreach的实现手段,如上文所述,我们并没有继承和实现那么多接口,所以List<T>实现Foreach的手段我们就无法使用了。但是想想Foreach的目的无法就是遍历的过程中进行一些自己需要的操作,所以这里我使用了delegate来实现这一点。同样,Find这样的功能也可以通过delegate来实现,关于Find的实现放在下面的代码中了。
好啦,上面就是这篇文章的内容了,因为断断续续写了一周所以内容有点多,如果都盛放在一篇里面,可能连小匹夫都要有点密集恐惧症了。那么在下一篇文章《自己动手,实现一种类似List<T>的数据结构(二)》中,小匹夫将详细介绍下小匹夫觉得有用且有趣的方法。