向量是一个可以动态改变长度的数组,起作用就是无需确定集合的初始长度,集合会随着存放数据的数量自行变化。
.net Framwork中提供了两种向量的规范:
- 集合元素类型为Object类型的IList接口,可以存放不同类型的对象引用;
- 以泛型为基础的IList<T>接口,只能存放一种类型的对象引用。
下图展示了IList接口及IList<T>接口的继承层次
图1 IList及IList<T>继承层次图
一、IList接口
1.1 接口特点:
IList接口从ICollection接口继承,所以其具备如下特性:
- 可获取集合元素个数 - Count属性;
- 可迭代 - GetEnumerator方法(从IEnumerable接口继承);
- 集合元素传递给数组 - CopyTo方法。
IList接口添加了如下特性:
- 访问集合任意索引所在元素 - this[]索引器属性;
- 向集合末尾添加元素 - Add方法;
- 向集合特定位置插入元素 - Insert方法;
- 删除特定元素 - Remove方法;
- 删除索引位置特定的元素 - RemoveAt方法;
- 判断对象是否在集合中被引用 - Contains方法;
- 查找特定对象在集合中的索引位置 - IndexOf方法;
- 清空整个集合 - Clear方法。
IList类型集合按照顺序存放元素(所谓顺序即IList类型集合不会改变元素的存放顺序)。
1.2 IList<T>接口
IList<T>接口从ICollection<T>接口继承,所以其具备如下特性:
- 可获取集合元素个数 - Count属性;
- 可迭代 - GetEnumerator方法(从IEnumerable接口继承);
- 集合元素传递给数组 - CopyTo方法;
- 向集合末尾添加元素 - Add方法;
- 删除特定元素 - Remove方法;
- 判断对象是否在集合中被引用 - Contains方法;
- 清空整个集合 - Clear方法。
IList<T>接口添加了如下特性:
- 访问集合任意索引所在元素 - this[]索引器属性
- 向集合特定位置插入元素 - Insert方法;
- 删除索引位置特定的元素 - RemoveAt方法;
- 查找特定对象在集合中的索引位置 - IndexOf方法;
IList<T>类型集合按照顺序存放特定类型元素。
二、算法
另外,向量集合和数组一样,具备随机访问特点。即无论访问向量集合的任何一个单元,所需的访问时间是完全相同的。C#的IList/IList<T>接口表现出C++中vector<T>的特性。
- 实际应用中,我们大多使用泛型向量,因为它更安全且高效。
在向量类中,依然使用普通数组来记录集合数据,只是向量使用了一些算法技巧,让整个类对外表现不同于普通数组的重要特点可以动态的改变数组长度,具体算法要点如下:
在内部数组长度足够的情况下,直接进行添加或插入操作(必要时需要数据搬移);
在内部数组长度不足的情况下,按照内部数组长度加1的2倍作为新数组的长度分配新数组,进行必要数据搬移,进行添加或插入操作;
在数据删除时,并不改变内部数组的长度,而仅仅是使用被删除数据之后的数据覆盖被删除数据即可。
向量再分配元素存储空间时,使用了典型的空间换时间原则,即每次空间不足的时候,都多分配一些冗余空间,以求最低限度的减少内存分配次数。内存分配是程序性能的一个瓶颈,应尽量避免集中的次数繁多的内存分配。
三、实现类
.net Framework提供了IList和IList<T>的实现类,它们是ArrayList类和List<T>类,其中:
- ArrayList类处于System.Collections命名空间下;
- List<T>类处于System.Collections.Specialized命名空间下。
四、一个IList接口的实现(源代码)
ArrayList.cs
- using System;
- using System.Collections;
- namespace Edu.Study.Collections.List {
- /// <summary>
- /// 定义向量集合类, 实现IList接口
- /// </summary>
- public class ArrayList : IList {
- /// <summary>
- /// 迭代子定义, 实现IEnumerator接口, 具备如下属性和方法:
- /// - Current属性, object类型属性, 获取集合的“当前”位置元素;
- /// - MoveNext方法, 移动到集合下一个元素, 返回是否移动到集合末尾的布尔值;
- /// - Reset方法, 返回初始状态。
- /// </summary>
- public struct Enumertor : IEnumerator {
- /// <summary>
- /// 表示迭代索引, 即迭代器访问到向量的下标
- /// </summary>
- private int index;
- /// <summary>
- /// 一个到迭代器所属的向量类对象的引用
- /// </summary>
- private ArrayList list;
- /// <summary>
- /// 参数构造器
- /// </summary>
- /// <param name="<a href="http://lib.csdn.net/base/docker" class='replace_word' title="Docker知识库" target='_blank' style='color:#df3434; font-weight:bold;'>Container</a>">表示迭代器所属的集合类</param>
- public Enumertor(ArrayList container) {
- this.list = container;
- this.index = -1;
- }
- /// <summary>
- /// 获取迭代器当前数据对象。
- /// 在向量中, 这个属性根据ArrayList.index字段的值, 返回向量对应位置的对象引用
- /// </summary>
- public object Current {
- get {
- return list[index];
- }
- }
- /// <summary>
- /// 将迭代器指示到“下一个数据位置”
- /// 在向量中, 这个方法将index字段的值加1(在这个值小于向量长度的前提下)
- /// </summary>
- /// <returns>当迭代器到达数据集合末尾时, 返回false</returns>
- public bool MoveNext() {
- if (this.index < list.Count) {
- ++this.index;
- }
- return this.index < list.Count;
- }
- /// <summary>
- /// 将迭代器指示恢复到集合起始位置。
- /// 在向量中, 这个方法将index字段的恢复为-1
- /// </summary>
- public void Reset() {
- this.index = -1;
- }
- }
- /// <summary>
- /// 保存集合的数组
- /// </summary>
- private object[] array;
- /// <summary>
- /// 当前集合的长度
- /// </summary>
- private int count;
- /// <summary>
- /// 默认构造器
- /// </summary>
- public ArrayList()
- : this(1) {
- }
- /// <summary>
- /// 参数构造器
- /// 提供这个参数构造器可以预先的初始化内部数组的长度, 减少内部数组重新分配
- /// 内存的次数, 提高效率。但参数设置过大则会浪费内存。
- /// </summary>
- /// <param name="capacity">初始化时内部数组的长度</param>
- public ArrayList(int capacity) {
- if (capacity < 0) {
- throw new ArgumentOutOfRangeException("caption");
- }
- if (capacity == 0) {
- capacity = 1;
- }
- this.array = new object[capacity];
- this.count = 0;
- }
- /// <summary>
- /// 获取向量的长度
- /// </summary>
- public int Count {
- get {
- return this.count;
- }
- }
- /// <summary>
- /// 返回向量实际使用的长度
- /// </summary>
- public int Capacity {
- get {
- return this.array.Length;
- }
- }
- /// <summary>
- /// 是否固定大小的属性(返回常量false)
- /// </summary>
- public bool IsFixedSize {
- get {
- return false;
- }
- }
- /// <summary>
- /// 是否只读集合的属性(返回常量false)
- /// </summary>
- public bool IsReadOnly {
- get {
- return false;
- }
- }
- /// <summary>
- /// 集合是否可同步属性, 返回false, 表示集合无法同步
- /// </summary>
- public bool IsSynchronized {
- get {
- return false;
- }
- }
- /// <summary>
- /// 同步对象属性, 返回null, 表示集合无需同步
- /// </summary>
- public object SyncRoot {
- get {
- return null;
- }
- }
- /// <summary>
- /// 当this.array长度不够时, 重新分配长度足够的新数组。
- /// 分配新数组的原则是:以原数组长度加 1 的 2 倍作为新数组的长度
- /// </summary>
- /// <returns>分配的新数组</returns>
- private object[] GetNewArray() {
- return new object[(this.array.Length + 1) * 2];
- }
- /// <summary>
- /// 向向量的末尾添加对象。
- /// 在发生内部数组长度不足的情况下, 程序按照(原有数组长度 + 1) * 2的原则产生新数组
- /// </summary>
- /// <param name="value">要添加的对象</param>
- public int Add(object value) {
- // 计算增加后集合长度
- int newCount = this.count + 1;
- // 判断当前数组数量是否能满足要求
- if (this.array.Length < newCount) { // 当数组长度不足
- // 分配新数组
- object[] newArray = GetNewArray();
- // 将原数组的元素复制到新数组中
- Array.Copy(this.array, newArray, this.count);
- // 重新设定内部数组字段的引用
- this.array = newArray;
- }
- // 添加新数据
- this.array[this.count] = value;
- this.count = newCount;
- // 返回添加项的索引位置
- return this.count - 1;
- }
- /// <summary>
- /// 索引器属性, 按索引返回向量中的某一项
- /// </summary>
- /// <param name="index">索引数</param>
- /// <returns>数组中第index项的对象引用</returns>
- public object this[int index] {
- get {
- if (index < 0 || index >= this.count) {
- throw new ArgumentOutOfRangeException("index");
- }
- return this.array[index];
- }
- set {
- if (index < 0 || index >= this.count) {
- throw new ArgumentOutOfRangeException("index");
- }
- this.array[index] = value;
- }
- }
- /// <summary>
- /// 删除向量中的元素
- /// </summary>
- /// <param name="index">开始删除的起始索引</param>
- /// <param name="count">要删除的元素的个数</param>
- public void RemoveRange(int index, int count) {
- if (index < 0) {
- throw new ArgumentOutOfRangeException("index");
- }
- // 计算最后一个被删除元素的索引位置
- int removeIndex = index + count;
- // 查看要删除的元素索引位置是否在合理范围内
- if (count < 0 || removeIndex > this.count) {
- throw new ArgumentOutOfRangeException("count");
- }
- // 将待删除元素其后的数据拷贝到待删除元素位置
- Array.Copy(this.array, index + 1, this.array, index + count - 1, this.count - removeIndex);
- // 重新设定向量长度
- this.count -= count;
- }
- /// <summary>
- /// 从集合中删除特定对象的引用
- /// </summary>
- /// <param name="value">要从集合中删除的对象引用</param>
- public void Remove(object value) {
- // 查找value在数组中所在的索引位置
- int index = this.IndexOf(value);
- // 如果找到了该元素所在的位置
- if (index >= 0) {
- // 删除索引位置处一个元素
- this.RemoveRange(index, 1);
- }
- }
- /// <summary>
- /// 从集合的特定位置删除对象的引用
- /// </summary>
- /// <param name="index">要删除对象引用所在的位置</param>
- public void RemoveAt(int index) {
- // 删除索引处一个元素
- RemoveRange(index, 1);
- }
- /// <summary>
- /// 查找对应的数组项
- /// </summary>
- /// <param name="o">待查找的对象</param>
- /// <returns>被查到的对象所在的索引位置, -1表示没有找到</returns>
- /// <remarks>该方法使用了顺序查找的方法逐一比较数组中的元素进行查找</remarks>
- public int IndexOf(object value) {
- // 注意, 这里面使用到了一个查询技巧, 以避免抛出异常
- // 由于数组中可以存在null值, 而参数value也可以为null, 所以查询比较要分为两种情况
- // - 当参数value为null时, 只需要从数组中找到第一个同为null的数组项即可;
- // - 当参数value为非null时, 则可以调用其Equals方法, 来逐一和数组项进行比较.
- // 一定要防止使用值为null的数组项或变量来调用任何属性或方法.
- int index = 0;
- if (value == null) { // 处理参数为null的情况
- while (index < this.count) {
- if (this.array[index] == null) {
- return index;
- }
- ++index;
- }
- } else { // 处理参数为非null的情况
- while (index < this.count) {
- if (value.Equals(this.array[index])) {
- return index;
- }
- ++index;
- }
- }
- // 未找到匹配项, 返回-1表示没找到
- return -1;
- }
- /// <summary>
- /// 弹出向量的最后一个元素(即获取最后一个元素的引用后删除最后一个元素)
- /// </summary>
- /// <returns>数组的最后一个对象</returns>
- public object PopBack() {
- object o = this.array[this.count - 1];
- RemoveAt(this.count - 1);
- return o;
- }
- /// <summary>
- /// 弹出向量的第一个对象
- /// </summary>
- /// <returns>数组的第一个对象</returns>
- public object PopFront() {
- object o = this.array[0];
- RemoveAt(0);
- return o;
- }
- /// <summary>
- /// 向数组的任意地方插入数据
- /// </summary>
- /// <param name="index">要插入的位置</param>
- /// <param name="value">要插入的值</param>
- public void Insert(int index, object value) {
- if (index >= this.count) {
- throw new ArgumentOutOfRangeException("index");
- }
- // 插入数据采用和添加数据(Add方法)相同的算法.
- // 即array字段长度不足以容纳新插入数据时, 实例
- // 化一个新数组, 长度是array字段数组长度 + 1的
- // 2倍.
- int newCount = this.count + 1;
- // 插入的原则是, 将数组元素从插入位置起向后全部移动一个位置
- // 将待插入的对象引用存入空余出的位置中
- if (this.array.Length < newCount) { // array字段数组容量不足
- object[] newArray = GetNewArray();
- Array.Copy(this.array, newArray, index);
- newArray[index] = value;
- Array.Copy(this.array, index, newArray, index + 1, this.count - index);
- this.array = newArray;
- } else { // array字段数组容量足够
- Array.Copy(this.array, index, this.array, index + 1, this.count - index);
- this.array[index] = value;
- }
- this.count = newCount;
- }
- /// <summary>
- /// 查看当前向量是否包含制定对象
- /// </summary>
- /// <param name="value">要查找的对象</param>
- /// <returns>是否包含指定对象</returns>
- public bool Contains(object value) {
- return this.IndexOf(value) >= 0;
- }
- /// <summary>
- /// 将内部数组的长度压缩为向量的实际长度
- /// </summary>
- public void TrimToSize() {
- // 从Add方法和Insert方法可以看出, 向量在添加元素时, 会产生大量的冗余
- // 这个方法就是消除冗余的
- // 判断是否产生了冗余(数组长度大于向量实际长度)
- if (this.array.Length > this.count) {
- object[] newArray = null;
- // 实例化一个和向量实际长度相同的数组
- // 将向量中的元素逐一存储到这个数组中
- // 替换掉array字段保存的旧数组引用
- if (this.count > 0) {
- newArray = new object[this.count];
- Array.Copy(this.array, newArray, this.count);
- } else {
- newArray = new object[1];
- }
- this.array = newArray;
- }
- }
- /// <summary>
- /// 清空向量
- /// </summary>
- public void Clear() {
- // 无需重新分配空间, 将count字段设置为0即可
- this.count = 0;
- }
- /// <summary>
- /// 获取该集合类的迭代器
- /// </summary>
- /// <returns>该集合类的迭代器</returns>
- public IEnumerator GetEnumerator() {
- Enumertor ator = new Enumertor(this);
- return ator;
- }
- /// <summary>
- /// 将集合中的元素复制到数组中
- /// </summary>
- /// <param name="array"></param>
- /// <param name="index"></param>
- public void CopyTo(Array array, int index) {
- Array.Copy(this.array, 0, array, index, this.count);
- }
- }
- }
仔细观察上述代码,弄清楚两个问题:
- 迭代子类(17 - 67行)是如何实现的,其内部原理是什么?
- 向量是如何存储对象引用的,存储分配的算法(163 - 164行)是什么?
另外还要注意的索引器的使用(203行),利用索引器可以表现出来更优美的语法;
可以采用如下代码测试ArrayList类:
Program.cs
- using System;
- namespace Edu.Study.Collections.List {
- class Program {
- static void Main(string[] args) {
- // 声明向量集合对象
- ArrayList lst = new ArrayList();
- // 添加两个元素
- lst.Add(10);
- lst.Add(20);
- // 使用for循环和索引器遍历集合
- for (int i = 0; i < lst.Count; ++i) {
- Console.WriteLine(lst[i]);
- }
- Console.WriteLine();
- // 使用迭代循环遍历集合
- foreach (object o in lst) {
- Console.WriteLine(o);
- }
- Console.WriteLine();
- // 像第2个元素前插入新元素
- lst.Insert(1, 100);
- foreach (object o in lst) {
- Console.WriteLine(o);
- }
- Console.WriteLine();
- // 删除值为100的元素(此处测试值类型,童鞋们可以自行测试引用类型)
- lst.Remove(100);
- foreach (object o in lst) {
- Console.WriteLine(o);
- }
- Console.WriteLine();
- // 删除第2个元素
- lst.RemoveAt(1);
- foreach (object o in lst) {
- Console.WriteLine(o);
- }
- Console.WriteLine();
- // 清空集合
- lst.Clear();
- foreach (object o in lst) {
- Console.WriteLine(o);
- }
- Console.WriteLine();
- // 添加10个随机数
- Random rand = new Random();
- for (int i = 0; i < 10; ++i) {
- lst.Add(rand.Next(10));
- }
- foreach (object o in lst) {
- Console.WriteLine(o);
- }
- // 测试查找
- Console.WriteLine("集合中是否包含值为0的元素?答案:" + (lst.Contains(0) ? "是" : "否"));
- Console.WriteLine("值为0的元素在第几个位置?答案:" + lst.IndexOf(0));
- }
- }
- }
-
顶
- 0
-
踩