1
概念

数组是一个存储相同数据类型的固定大小的顺序集合,允许将多个数据项当作一个集合来处理的数据结构。

数组的声明
//dataType[] arrayName
int[] intArray;
//初始化
intArray = new int[3];

第一行代码声明了一个int类型的数组,此时并没有分配数组,所以默认赋值为null。
第二行代码将会在托管堆上分配容纳3个未装箱的int值的内存块,并默认为所有值赋值为0。

引用类型数组
class People { }

People[] peopleArray;
//全部初始化为null,未分配值,相当于创建了一组引用
peopleArray = new People[3];

由于数组是引用类型,所以不论是值类型数组还是引用类型数组,都是存在于托管堆中的,值类型数组和引用类型数组在托管中的情况如图:
image.png

上图的People数组,就是执行了以下代码的结果

peopleArray[0] = new Man();
peopleArray[1] = new Woman();
peopleArray[2] = new Child();
数组类型
  • 0基数组(索引从0开始)
  • 多维数组(包括一维数组)
  • 交错数组(由数组构成的数组)
声明多维数组
int[,] intArrays = new int[2, 2]; //二维数组
string[,,] strArrays = new string[1, 2, 2]; //三维数组
声明交错数组
int[][] interleavedArray = new int[3][];
interleavedArray[0] = new int[1];
interleavedArray[1] = new int[2];
interleavedArray[2] = new int[3];
将数组的声明与初始化合并
int[] intArray = new int[] { 1, 2, 3 };
int[,] intArrays = new int[,] { { 1, 2 } };//二维
//简化
var intArray = new int[] { 1, 2, 3 };
//继续简化
var intArray = new[] { 1, 2, 3 };

*在使用隐式类型的数组功能时(不指定数组的具体类型),编译器会检查用于初始化数组元素的类型,并选择所有元素最接近的共同基类作为数组的类型,如果其中某一个元素与其它不具有相同类型或基类型编译器将报错:找不到隐式类型数组的最佳类型。

隐式类型数组和匿名类型相结合
var peoples = new[] { new { Name = "A" }, new { Name = "B" } };
//输出
//A
//B
foreach (var item in peoples)
    Console.WriteLine(item.Name);

*使用隐式类型数组需保证类型的同一性

数组协变性

数组协变性也称数组的转型,CLR允许将引用类型的数组从一种类型隐式转换成另一种类型,数组转型需要具备以下条件:

  • 两个数组类型必须维数相同
  • 源类型到目标类型必须存在一个显示或隐式的转换

*CLR不允许将值类型元素的数组转型成其它任何类型

People[] peopleArray;
object[] objectArray = peopleArray; //正确

int[,] intArrays = new int[,] { { 1, 2 } };
object[,] objectArray = intArrays; //错误
Array.Copy变相实现值类型元素数组的转型
var intArray = new[] { 1, 2, 3 };
object[] objectArray = new object[intArray.Length];
Array.Copy(intArray, objectArray, intArray.Length);
Array.Copy方法能执行以下转换:
  • 将值类型的元素装箱成为引用类型的元素
  • 将引用类型的元素拆箱成为值类型的元素

*由于Copy方法会产生装箱或者拆箱,毋庸置疑会对性能产生影响,使用Array.ConstrainedCopy方法将不会产生装箱拆箱操作,同样也有相对的限制:源数组类型元素与目标数组元素类型相同或者派生自同一个基类。

数组的传递和返回建议

数组是引用类型,所以数组作为参数传递给方法时,传递的其实是引用,所以在方法内部对数组进行操作的结果将会直接影响源数据。
方法在返回一个数组时,最好不要返回null,即使这个数组是空数组,原因如下:

int[] intArray = new int[] { };
//遍历空数组
foreach (var item in intArray)
    Console.WriteLine(item);
    
int[] intArray = null;
//遍历值为null的数组
if (intArray != null)
{
    foreach (var item in intArray)
    Console.WriteLine(item);
}

使用CreateInstance创建下限非0的数组,该方法允许指定数组元素的类型,数组的维数,每一维的下限和每一维的元素数目

int[] lowerBounds = { 3, 1 }; //每一维数数组的下限
int[] lengths = { 3, 2 }; //每一维数数组的长度
Decimal[,] decimalArray = (Decimal[,])Array.CreateInstance(typeof(Decimal), lengths, lowerBounds);
decimalArray[3, 1] = 1;
decimalArray[3, 2] = 2;
for (int i = decimalArray.GetLowerBound(0); i <= decimalArray.GetUpperBound(0); i++)
{
    for (int j = decimalArray.GetLowerBound(1); j <= decimalArray.GetUpperBound(1); j++)
    {
        Console.WriteLine("i : {0}, j : {1}, value : {2}", i, j, decimalArray[i, j]);
    }
}

输出:
image.png

创建一维的非0基数组
int[] myArrLen = { 4 };
int[] myArrLow = { 2 };
var myArrayTwo = Array.CreateInstance(typeof(int), myArrLen, myArrLow);
//赋值
myArrayTwo.SetValue(1, 2);
myArrayTwo.SetValue(2, 3);
myArrayTwo.SetValue(3, 4);
myArrayTwo.SetValue(4, 5);
//取值
myArrayTwo.GetValue(4);

*必须使用Array的SetValue与GetValue方法访问一维非0基数组的元素

访问数组的性能

在遍历0基一维数组的时候,通常需要访问数组的Length,由于JIT编译器知道Length是Array的属性,所以不会把它当作一个方法每次循环都调用,而会在第一次循环的时候调用一次,并将值存储到一个临时变量中,之后每次循环检查这个临时变量即可,从而提升性能。
而访问非0基一维数组或多维数组的时候,JIT编译器不会将索引检查从循环中抽出来,导致每次循环都得验证指定的索引,从而影响代码的速度,这也是性能不如0基一维数组的原因。

使用交错数组和unsafe提升数组访问性能
//多维数组
int[,] intArrays = new int[10000,10000];
var sw = Stopwatch.StartNew();
var sum = 0;
//普通安全遍历
for (int i = 0; i < 10000; i++)
    for (int j = 0; j < 10000; j++)
        sum += intArrays[i, j];
Console.WriteLine("遍历二维数组 耗时: {0}", sw.Elapsed);

//交错数组
int[][] interleavedArray = new int[10000][];
sw = Stopwatch.StartNew();
//普通安全遍历
for (int i = 0; i < 10000; i++)
    for (int j = 0; j < 10000; j++)
        sum += intArrays[i, j];
Console.WriteLine("遍历交错数组 耗时: {0}", sw.Elapsed);

//unsafe遍历
sw = Stopwatch.StartNew();
Test(intArrays);
Console.WriteLine("unsafe遍历数组 耗时: {0}", sw.Elapsed);

private static unsafe void Test(int[,] arg)
{
    var sum = 0;
    fixed(Int32 * pi = arg)
    {
        for (int i = 0; i < 10000; i++)
        {
            var temp = i * 10000;
            for (int j = 0; j < 10000; j++)
                sum += pi[temp + j];
        }
    }
}

image.png

测试结果可以得出:
遍历二维数组最慢,交错数组安全遍历时间少于安全遍历二维数组的耗时,但是由于创建交错数组需要在堆上为每一维分配一个对象,造成垃圾回收,所以创建交错数组所花费的时间要大于创建二维数组的时间。
因此我们可以在需要创建大量多维数组时,而不会频繁访问数组中的元素,那么选择创建多维数组性能较好。反之如果多维数组只需创建一次,并且需要频繁访问数组中的元素,那么就是用交错数组性能来得更好一些。

显然不安全代码是性能最好的,但是使用该技术同样存在风险

  • unsafe直接操作内存,存在破坏类型安全的风险
  • 代码复杂,不易写
  • 使用fixed,需要执行内存地址计算可读性降低
  • 如果内存地址计算错误,会损坏内存数据,破坏类型安全

DoubleJ
7 声望3 粉丝

« 上一篇
索引器
下一篇 »
委托