数组使用---进阶编程篇
Posted dathlin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数组使用---进阶编程篇相关的知识,希望对你有一定的参考价值。
本篇文章讲解数组的使用,先是介绍下几种不同的数组,在说明下各自的区别和使用场景,然后注意细节,废话不多说,赶紧上代码。
在.Net 3.5之中,我们常用的数组基本就是如下的几种方式(词典Dictionary<TKey,TValue>比较特殊,下面单独解释):
- ArrayList 方式的数组
- T[] 方式的数组
- List<T> 方式数组
- Queue<T> 先进先出的数组
- Stack<T> 后进先出的数组
ArrayList:
简单的说,ArrayList是一个微软早期提供的数组类型,在那个时代还没有泛型的时候,我们需要一个可变数组的时候,就会采用这个数组,现在来说,已经很少使用了,不排除有些场景特别适用这个数组,如果要说这个数组的优点,恐怕只有一个,还是最大的一个优点就是对数组成员类型不确定,所以我们可以这么写代码:
1 private void button1_Click(object sender, EventArgs e) 2 { 3 ArrayList arrayList = new ArrayList(); 4 arrayList.Add(false); 5 arrayList.Add(5); 6 arrayList.Add(5.3); 7 arrayList.Add("测试数据"); 8 arrayList.Add(Guid.NewGuid()); 9 arrayList.Add(DateTime.Now); 10 11 12 foreach(var m in arrayList) 13 { 14 textBox1.AppendText(m.GetType().ToString() + Environment.NewLine); 15 } 16 17 }
显示结果为:
这种方式和我们的一般思维不太一样,我们一般定义一组数据时,肯定是一样的类型的呀,如果按照ArrayList类型来看,我们每次调用中间一个成员的时候,还得判断下什么类型,因为它很可能不是简单的Object,如果我们在代码中都是Add同一种类型,那么是否就意味着使用的时候不需要进行类型判定了吗?肯定不行,因为你一旦在代码的其他地方使用了Add方法,并添加了一个不是你期望的类型对象(编译器根本不会提示错误),这会对以后你查找问题产生极大的阻碍。
这种数组还有巨大的缺陷。就是频繁的拆箱装箱,如果只有100长度以内的数据,对性能的损耗也许会看不出来,但是数组长度达上万的时候,绝对会成为性能的瓶颈,关于拆箱装箱的描述,在其他很多的书上都会有描述,如果需要说清楚,又得回到区分值类型和引用类型的区别,又会扯出线程栈和托管堆的区别,暂时就不深入了。所以基本不选择这个数组。
T[] 数组
这是一个最常用的数组类型了,其实这种数组非常的好用和扩展功能,我们非常习惯于下面的定义方式:
1 private void button2_Click(object sender, EventArgs e) 2 { 3 int[] temp = new int[100]; 4 Button[] buttones = new Button[100]; 5 6 // 正常访问 7 foreach (var m in temp) 8 { 9 textBox1.AppendText(m.ToString() + Environment.NewLine); 10 } 11 12 // 异常,抛出NullReferenceException 13 foreach (var m in buttones) 14 { 15 textBox1.AppendText(m.Text + Environment.NewLine); 16 } 17 18 }
在上述的例子中,存在一个细微的差别,如果你定义的是非空的值类型数据,会自动的赋初值,也即是default(int),也即是0,而引用类型的初值是NULL,但无论怎么说,buttones确实一个长度为100的数组,只是里面的数据都为空罢了,所以我们可以这么写:
1 Button[] buttones = new Button[100]; 2 buttones[99] = new Button();
这样我们只实例化数组的最后一个对象。
List<T> 数组对象
这个数组对象厉害了,这个数组自从C#引入了泛型以来就立即被广泛引用,它继承了ArrayList的优良传统,又解决掉了ArrayList留下来的所有弊端,基本上可以说一劳永逸了,它甚至更强大的是带来了Lambada表达式,这真是一个巨大的进步,下面就来详细的介绍下C#中大名鼎鼎的List<T>数组了,先从实例化开始好了,List<T>的数组实例化小个小细节,如果我们要实例化一个包含了100万个int的数组,应该怎做,和int[] 初始化有什么区别:
1 private void button3_Click(object sender, EventArgs e) 2 { 3 int[] list1 = new int[1000000]; 4 // 可以直接按下面这么用 5 list1[10000] = 10; 6 7 List<int> list = new List<int>(1000000); 8 // 下面的代码会引发异常ArgumentOutOfRangeException 9 list[10000] = 10; 10 }
如下两种方式实例化一个长度1000000个0的List<int>实例:
1 private void button5_Click(object sender, EventArgs e) 2 { 3 // 方式一 4 List<int> list1 = new List<int>(); 5 for(int i=0;i<1000000;i++) 6 { 7 list1.Add(0); 8 } 9 10 // 方式二 11 List<int> list2 = new List<int>(new int[1000000]); 12 }
以下展示一个Lambada表达式的巨大好处,假设有个List<int>数组,包含了10000个数据,我们需要筛选出里面所有大于100的数据并重新生成一个数组,以下展示2中写法,你就能明白中间的区别在哪里了
1 private void button6_Click(object sender, EventArgs e) 2 { 3 // 生成一万个随机数据(0-200)的数组 4 List<int> list = new List<int>(); 5 Random r = new Random(); 6 for(int i=0;i<10000;i++) 7 { 8 list.Add(r.Next(200)); 9 } 10 11 // 方式一 12 List<int> result1 = new List<int>(); 13 for (int i = 0; i < list.Count; i++) 14 { 15 if (list[i] > 100) 16 { 17 result1.Add(list[i]); 18 } 19 } 20 21 // 方式二 22 List<int> result2 = list.Where(m => m > 100).ToList(); 23 }
无论从代码的工作量上来说还是理解度来说,都是第二种方式完胜,而且两者的性能相差无几,所以极度推荐大家学好委托和Lambada表达式。
讲完了List<T>类型的几种用法,来说说原理的东西,我刚学习并用了一段时间的List<T>后,就特别好奇微软怎么实现了List<T>对象,所谓的动态长度的数组原理是什么,为什么可以使用Add方法来新增Item,带着这些疑问,后来看到了微软开源出来的代码,List<T>的开源地址为http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,2765070d40f47b98
源代码非常的长,这里就不全复制了,有兴趣的可以看看,上面有很多细节可以学习,我就大致讲一下List<T>的根本原理,有助于大家理解,首先List<T>内部使用的底层数据仍然是T[]类型的,并且声明了一个初始容量4,在执行了List<int> list = new List<int>(1000000)这段代码后,部分真的有一个 _items 为int[10000]的对象,但是我们为什么在使用list[10000]=10的时候异常了呢?因为里面还有个数据容量 _size,在初始化的时候并没有赋值;而当你使用list[10000]时,下面的代码足以说明问题:
1 // Sets or Gets the element at the given index. 2 // 3 public T this[int index] { 4 get { 5 // Following trick can reduce the range check by one 6 if ((uint) index >= (uint)_size) { 7 ThrowHelper.ThrowArgumentOutOfRangeException(); 8 } 9 Contract.EndContractBlock(); 10 return _items[index]; 11 } 12 13 set { 14 if ((uint) index >= (uint)_size) { 15 ThrowHelper.ThrowArgumentOutOfRangeException(); 16 } 17 Contract.EndContractBlock(); 18 _items[index] = value; 19 _version++; 20 } 21 }
源代码里还有细节值得注意,当我们调用了Add(1000)方法时,发生了什么事情,如下的源代码来源于微软
1 public void Add(T item) { 2 if (_size == _items.Length) EnsureCapacity(_size + 1); 3 _items[_size++] = item; 4 _version++; 5 } 6 private void EnsureCapacity(int min) { 7 if (_items.Length < min) { 8 int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2; 9 // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. 10 // Note that this check works even when _items.Length overflowed thanks to the (uint) cast 11 if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; 12 if (newCapacity < min) newCapacity = min; 13 Capacity = newCapacity; 14 } 15 } 16 public int Capacity { 17 get { 18 Contract.Ensures(Contract.Result<int>() >= 0); 19 return _items.Length; 20 } 21 set { 22 if (value < _size) { 23 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity); 24 } 25 Contract.EndContractBlock(); 26 27 if (value != _items.Length) { 28 if (value > 0) { 29 T[] newItems = new T[value]; 30 if (_size > 0) { 31 Array.Copy(_items, 0, newItems, 0, _size); 32 } 33 _items = newItems; 34 } 35 else { 36 _items = _emptyArray; 37 } 38 } 39 } 40 }
源代码中的两个方法加一个属性已经表示的很清楚了,假设list原来初始容量为4,我们刚好add了4个值,当我们add第五个值的时候,就会发生很多事情
- 判断原来的数组容量够不够?因为数量已经超了,所以不够
- 因为原数组不够,所以需要进行扩充,扩充多少呢?原数组长度*2!!!!!,此处为8
- 确定好了扩充的数据,重新生成一个8长度的数组
- 准备复制数据,值类型复制数值本身,而引用类型仅仅复制引用,此处把原来4个旧的数据复制到8个新数组的前四个位置上
- 最后的最后list[5]=10;因为这时候的长度已经足够了,允许安全的赋值
所以这个操作还是非常恐怖的,假设这时候数组的长度已经100万了,再新增一个数据,将要生成200万长度的数据,再挪一百万长度的数据,虽然说挪一次的性能还是非常高的,这里为什么要按两倍扩充呢,估计也是为了性能考虑,假设你的list使用Add方法调用了100万次,实际只是扩充了19次(可能18次,没具体算),所以这么看下来如果确定数组的长度是固定的情况,使用 T[] 的性能最好。
Queue<T> 数组类型
这个数组对象和List<T>非常的像,在绝大多数情况下都可以用List<T>来替代实现,如下的代码演示了同时添加一个数据,和移除一个数据时的操作:
1 private void button7_Click(object sender, EventArgs e) 2 { 3 Queue<int> queue = new Queue<int>(); 4 List<int> list = new List<int>(); 5 6 // 两者等同 7 queue.Enqueue(1000); 8 list.Add(1000); 9 10 // 两者有一点微小的区别 11 int i = queue.Dequeue(); 12 int j = list[0]; 13 list.RemoveAt(0); 14 }
如果此处不需要获取被移除数据的数值的话,这里的代码就几乎等同了,所以这里适用的场景其实也很明显了,比如我们有一个消息队列,存放了消息对象,每个对象包含了发送人,发送时间,接收人,发送内容等等,来自各个地方的消息统一压入队列,专门由一个线程出处理这些消息,处理的模型就是拿一个处理一个,肯定是先发先处理了,这种情况就特别适合使用Queue<T>队列了。但是这时候又该考虑另一个问题了,那就是同步,在本篇下面将介绍。
Stack<T> 数组类型
不得不说,这个对象和List<T>, Queue<T> 都是非常的像,无非就是先入后出罢了,只要前两个理解了,这个就什么难度,至于实际中的应用场景,暂时还没想到,不过有一个技术真的超级适合先入后出的方式,就是指针,指针在执行到调用方法时,会将当前的位置压入堆栈,然后跳转到其他方法执行,执行完毕后,取出堆栈的值,跳到相应的地址继续执行。即使方法里再跳方法,方法里再跳方法,执行的步骤也不会乱。
使用注意:
ArrayList妙用:
如果你获取到一个数据Object 对象,但是知道它是一个数组,可能是bool[], 也可能是int[], double[], sting[]等等,现在要在表格中显示,就特别适合使用ArrayList类型
1 private void button8_Click(object sender, EventArgs e) 2 { 3 // 获取到的对象 4 Object obj = new object(); 5 6 if(obj is ArrayList list) 7 { 8 foreach(object m in list) 9 { 10 // 可以进行显示一些操作 11 } 12 } 13 }
排序性能:
有时候我们需要对一个数组进行排序,从小到大也好,从大到小也好,大学里教的都是冒泡法之类的,其实根本不要去使用,性能超差。
1 private void button9_Click(object sender, EventArgs e) 2 { 3 // 生成100000个数据长度的数组 4 int[] data = new int[100000]; 5 // 随机赋值 6 Random r = new Random(); 7 for (int i = 0; i < data.Length; i++) 8 { 9 data[i] = r.Next(1000000); 10 } 11 12 DateTime start = DateTime.Now; 13 14 // 开始排序 15 for (int i = 0; i < data.Length; i++) 16 { 17 for (int j = i + 1; j < data.Length; j++) 18 { 19 if (data[j] < data[i]) 20 { 21 int temp = data[j]; 22 data[j] = data[i]; 23 data[i] = temp; 24 } 25 } 26 } 27 28 textBox1.AppendText((DateTime.Now - start).TotalMilliseconds + Environment.NewLine); 29 30 // 重新赋值 31 for (int i = 0; i < data.Length; i++) 32 { 33 data[i] = r.Next(1000000); 34 } 35 36 start = DateTime.Now; 37 // 第二种从小到大的排序 38 Array.Sort(data); 39 40 textBox1.AppendText((DateTime.Now - start).TotalMilliseconds + Environment.NewLine); 41 ; 42 }
实际消耗的时间差别巨大,冒泡法居然用了整整34秒钟,而系统自带排序算法只用了15毫秒,整个时间竟然差了2200倍以上。
同步问题:
这个问题算是最难也是最容易出问题的地方,而且很多问题还出的莫名其妙,有时出问题,有时又不出,关键的是在于对多线程的理解错误,在绝大多数的单线程应用程序中,几乎没有同步问题,比如你在窗口类中定义了一个数组,在窗口类中其他地方可以随意的使用数组,更改值也好,取出数值也好,求取平均值也好。程序都可以很好的工作,但是对于多线程的应用程序,我们很容易想到这样的处理模型。
我们建一个缓存的中间对象,比如int[] temp=new int[100],然后有一个线程从其他对方获取数据,可能来自设备,可能来自数据库,网络等等,获取数据后,对数组进行更新数据,然后其他地方对数组数据进行处理,获取值进行显示,计算平均数显示等等,所以我们很容易这么写代码:
1 private int[] arrayData = new int[100]; // 缓存数组 2 System.Windows.Forms.Timer timer = null; // 定时器 3 private void button10_Click(object sender, EventArgs e) 4 { 5 // 开线程更新数据 6 Thread thread = new Thread(UpdateArray); 7 thread.IsBackground = true; 8 thread.Start(); 9 10 // 在主界面开定时器访问数组显示或计算等等 11 timer = new System.Windows.Forms.Timer(); 12 timer.Tick += Timer_Tick; 13 timer.Interval = 1000; // 每秒更新一次 14 timer.Start(); // 启动定时器 15 } 16 17 18 private void UpdateArray() 19 { 20 // 每隔200ms更新一次数据,先全部置1,然后全部置2 21 int jj = 1; 22 while (true) 23 { 24 Thread.Sleep(200); 25 for (int i = 0; i < arrayData.Length; i++) 26 { 27 arrayData[i] = jj; 28 } 29 jj++; 30 } 31 } 32 33 34 private void Timer_Tick(object sender, EventArgs e) 35 { 36 textBox1.Text = "总和:" + arrayData.Sum() + " 平均值:" + arrayData.Average(); 37 }
咋一看,没什么问题,然后运行调试,也没什么问题,就可以让它一直在现场跑程序,24小时不间断,然后突然有一天就挂了,抛出了异常,这个异常是新手经常忽略的地方,也比较难以找到。究其根本原因是,Sum()和Average()操作是需要对数组进行迭代的,而在迭代操作的同时是不允许更改数组的,那我们可以绕过迭代吗?,答案当然是可以的:
1 private void Timer_Tick(object sender, EventArgs e) 2 { 3 textBox1.Text = "总和:" + SumArrayData() + " 平均值:" + AverageArrayData(); 4 } 5 6 private int SumArrayData() 7 { 8 int sum = 0; 9 for (int i = 0; i < arrayData.Length; i++) 10 { 11 sum += arrayData[i]; 12 } 13 return sum; 14 } 15 16 private double AverageArrayData() 17 { 18 int sum = SumArrayData(); 19 return sum * 1.0 / arrayData.Length; 20 }
将需要迭代的代码改用for循环来获取计算,这样就不会发生异常了,即时24运行程序,也不会抛出异常,但是回过头来思考,为什么迭代会抛出异常?
为了数据安全!
假设我们需要计算总和,我们需要将数组的项目一个个相加,当我加到一半的时候,更改了数据,然后我们相加得到的总和中就会有一半的数据是旧的,另一半的数据是新的,最终获取到的数据就不是一次正常的数据,如果你的程序仅仅用来显示,那么问题不大,如果用来处理一些重要的逻辑业务,那么问题就大了,会直接带来安全漏洞。带来数据的不准确,以至于后面根据该数据做出的决策全部都错误,比如说根据平均值进行报警,操作设备。所以进行迭代操作抛出异常是合情合理的。
这是一个多么值得深思的问题,这个问题不仅仅是针对T[] 数组类型的,还针对了上述所有的数组类型,因为他们都不是线程安全的。
所以,但凡碰到一个数组需要进行多线程操作的时候,必然加锁,来进行线程间的同步问题,一般我们比较能想到的就是lock语法糖,更高级的锁我们以后的文章再提及,所以上述的代码,我们可以将数组改造成线程安全的方式:
1 private int[] arrayData = new int[100]; // 缓存数组 2 System.Windows.Forms.Timer timer = null; // 定时器 3 private object lock_array = new object(); // 添加的锁 4 private void button10_Click(object sender, EventArgs e) 5 { 6 // 开线程更新数据 7 Thread thread = new Thread(UpdateArray); 8 thread.IsBackground = true; 9 thread.Start(); 10 11 // 在主界面开定时器访问数组显示或计算等等 12 timer = new System.Windows.Forms.Timer(); 13 timer.Tick += Timer_Tick; 14 timer.Interval = 1000; // 每秒更新一次 15 timer.Start(); // 启动定时器 16 } 17 18 19 private void UpdateArray() 20 { 21 // 每隔200ms更新一次数据,先全部置1,然后全部置2 22 int jj = 1; 23 while (true) 24 { 25 Thread.Sleep(200); 26 lock (lock_array) 27 { 28 for (int i = 0; i < arrayData.Length; i++) 29 { 30 arrayData[i] = jj; 31 } 32 jj++; 33 } 34 } 35 } 36 37 38 private void Timer_Tick(object sender, EventArgs e) 39 { 40 textBox1.Text = "总以上是关于数组使用---进阶编程篇的主要内容,如果未能解决你的问题,请参考以下文章