元素数量可变的情况下不应使用数组

继上一篇“使用dynamic来简化反射实现”后,本文继续看《编写高质量代码改善C#程序的157个建议》一书第16个建议“元素数量可变的情况下不应使用数组”。喜欢本书请到各大商城购买原书,支持正版。

正文开始:


关于集合和LINQ

软件开发过程中不可避免会用到集合,C#中的集合表现为数组和若干集合类。不管是数组还是集合类,它们都有各自的优缺点。如何使用好集合是我们在开发过程中必须掌握的技巧。不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行。

LINQ (Language Integrated Query,语言集成查询)提供了类似于SQL的语法,能对集合进行遍历、筛选和投影。一旦掌握了LINQ,你就会发现在开发中再也离不开它。

本章给出了C#开发中关于集合和LINQ的若干建议,以便你快速高效地针对集合进行编码。

建议16:元素数量可变的情况下不应使用数组

在C#中,数组一旦被创建,长度就不能改变。如果我们需要一个动态且可变长度的集合,就应该使用ArrayList或List<T>来创建。而数组本身,尤其是一维数组,在遇到要求高效率的算法时,则会专门被优化以提升其效率。一维数组也称为向量,其性能是最佳的,在IL中使用了专门的指令来处理它们(如newarr、ldelem、ldelema、ldlen和stelem)。

从内存使用的角度来讲,数组在创建时被分配了一段固定长度的内存。如果数组的元素是值类型,则每个元素的长度等于相应的值类型的长度﹔如果数组的元素是引用类型,则每个元素的长度为该引用类型的IntPtr.Size。数组的存储结构一旦被分配,就不能再变化。而ArrayList是链表结构,可以动态地增减内存空间,如果ArrayList存储的是值类型,则会为每个元素增加12字节的空间,其中4字节用于对象引用,8字节是元素装箱时引入的对象头。List<T>是ArrayList的泛型实现,它省去了拆箱和装箱带来的开销。


注意 由于数组本身在内存上的特点,因此在使用数组的过程中还应该注意大对象的问题。所谓“大对象”,是指那些占用内存超过85 000字节的对象,它们被分配在大对象堆里。大对象的分配和回收与小对象相比,都不太一样,尤其是回收,大对象在回收过程中会带来效率很低的问题。所以,不能肆意对数组指定过大的长度,这会让数组成为一个大对象。


如果一定要动态改变数组的长度,一种方法是将数组转换为ArrayList或 List<T>,如下面的代码所示:

int[] iArr = { 0, 1, 2, 3, 4, 5, 6 };
ArrayList arrayListInt = ArrayList.Adapter(iArr); //将数组转变为ArrayList
arrayListInt.Add(7);
Liste<int> listInt = iArr.ToList<int>();          //将数组转变为List<T>
listInt.Add(7);

还有一种方法是用数组的复制功能。数组继承自System.Array,抽象类System.Array提供了一些有用的实现方法,其中就包含了Copy方法,它负责将一个数组的内容复制到另外一个数组中。无论是哪种方法,改变数组长度就相当于重新创建了一个数组对象。

为了让数组看上去本身就具有动态改变长度的功能,还可以创建一个名为ReSize的扩展方法,代码如下所示:

public static class ClassForExtensions
{
  public static Array ReSize(this Array array, int newSize)
  {
    Type t = array.GetType().GetElementType();
    Array newArray = Array.CreateInstance(t, newsize);
    Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newSize));
    return newArray;
  }
}

调用方的代码看起来如下所示:

int [] iArr = { 0, 1, 2, 3, 4, 5, 6 };
iArr = (int [])iArr.Resize(10);

下面对改变数组长度和改变List<T>长度的耗时做一个比较,以便强调本建议的主题:在元素数量可变的情况下不应使用数组。

static void Main(string[] args)
{
  ResizeArray();
  ResizeList();
}

private static void ResizeArray()
{
  int[] iArr = { 0, 1, 2, 3, 4, 5, 6 };
  Stopwatch watch = new Stopwatch();
  watch.Start();
  iArr = (int[])iArr.Resize(10);
  watch.Stop();
  Console.WriteLine("ResizeArray: " + watch.Elapsed);
}

private static void ResizeList()
{
  List<int> iArr = new List<int>(new int [] { 0, 1, 2, 3, 4, 5, 6 });
  Stopwatch watch = new Stopwatch();
  watch.Start();
  iArr.Add(0);
  iArr.Add(0);
  iArr.Add(0);
  watch.Stop();
  Console.WriteLine("ResizeList: " + watch.Elapsed);
}

输出为:

ResizeArray: 00:00:00.0004441
ResizeList: 00:00:00.0000036

当然,严格意义上讲,List<T>不存在改变长度这种说法,本建议只是为了比较,将iArr的长度变为10,同时还进行了赋值。即便是这样,我们也可以看到,在时间效率上ResizeList 比 ResizeArray要高100倍以上。


下一篇我们接着读第2章 集合和LINQ的”建议17:多数情况下使用foreach进行循环遍历”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:https://mp.weixin.qq.com/s/2O7IPHDv3vM_6XPzNaEG7A

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论