首页 > 学院 > 开发设计 > 正文

[CLR via C#]16. 数组

2019-11-17 03:20:09
字体:
来源:转载
供稿:网友

[CLR via C#]16. 数组

  数组是允许将多个数据项当作一个集合来处理的机制。CLR支持一维数组、多维数组和交错数据(即由数组构成的数组)。所有数组类型都隐式地从System.Array抽象类派生,后者又派生自System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在你应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚的说明了这一点:

Int32[] myIntegers; //声明一个数组引用myIntegers = new int32[100] //创建含有100个Int32的数组

  在第一行代码中,myIntegers变量能指向一个一维数组(由Int32值构成)。myIntegers刚开始被设为null,因为当时还没有分配数组。第二行代码分配了含有100个Int32值的一个数组,所有Int32都被初始化为0。由于数组是引 用类型,所有托管堆上还包含一个未装箱Int32所需要的内存块。实际上,除了数组元素,数字对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员(overhead)。该数组的内存块地址被返回并保存到myIntegers变量中。

  C#也支持多维数组。下面演示了几个多维数组的例子:

// 创建一个二维数组,由Double值构成Double[,] myDoubles = new Double[10,20];// 创建一个三位数组,由String引用构成String[,,] myStrings = new String[5,3,10]; 

  CLR还支持交错数组,即由数组构成的数组。下面例子演示了如何创建一个多边形数组,其中每一个多边形都由一个Point实例数组构成。

// 创建一个含有Point数组的一维数组Point[][] myPolygons = new Point[3][];// myPolygons[0]引用一个含有10个Point实例的数组myPolygons[0] = new Point[10];// myPolygons[1]引用一个含有20个Point实例的数组myPolygons[1] = new Point[20];// myPolygons[2]引用一个含有30个Point实例的数组myPolygons[2] = new Point[30];// 显示第一个多边形中的Pointfor (Int32 x =0 ; x < myPolygons[0].Length; x++){  Console.WriteLine(myPolygons[0][x]);}

  注意:CLR会验证数组索引的有效性。换句话说,不能创建一个含有100个元素的数组(索引编号为0到99),又试图访问索引为-5或100的元素。

一、始化数组元素

  前面展示了如何创建一个数组对象,以及如何初始化数组中的元素。C#允许用一个语句来同时做两件事。例如:

String[] names = new String[] { "Aidan", "Grant" };

  大括号中的以逗号分隔的数据成为数组初始化器。每个数据项都可以是一个任意复杂度的表达式;在多维数组的情况下,则可以是一个嵌套的数组初始化器。可利用C#的隐式类型的数组功能让编译器推断数组元素的类型。注意,下面这一行代码没有在new和[]之间指定类型:

var names = new[] { "Aidan", "Grant", null};

  在上一行中,编译器检查数组中用于初始化数组元素的表达式的类型,并选择所有元素最接近的共同基类作为数组的类型。在本例中,编译器发现两个String和一个null。由于null可隐式转型成为任意引用类型(包括String),所以编译器推断应该创建和初始化一个由String引用构成的数组。

给定一下代码:

var names = new[] { "Aidan", "Grant", 123};

  编译器是会报错的,虽然String类和Int32共同基类是Object,意味着编译器不得不创建Object引用了一个数组,然后对123进行装箱,并让最后一个数组元素引用已装箱的,值为123的一个Int32。但C#团队认为,隐式对数组元素进行装箱是一个代价昂贵的操作,所以要做编译时报错。

  在C#中还可以这样初始化数组:

String[] names = { "Aidan", "Grant" };

  但是C#不允许在这种语法中使用隐式类型的局部变量:

var names = { "Aidan", "Grant" };

  最后来看下"隐式类型的数组"如何与"匿名类型"和"隐式类型的局部变量"组合使用。

// 使用C#的隐式类型的局部变量、隐式类型的数组和匿名类型var kids = new[] {new { Name="Aidan" }, new { Name="Grant" }};// 示例用法foreach (var kid in kids)  Console.WriteLine(kid.Name);

输出结果:

AidanGrant

二、数组转型  对于元素为引用类型的数组,CLR允许将数组元素从一种类型隐式转型到另一种类型。为了成功转型,两个数组类型必须维数相等,而且从源类型到目标类型,必须存在一个隐式或显示转换。CLR不允许将值类型元素的数组转型为其他任何类型。(不过为了模拟实现这种效果,可利用Array.Copy方法创建一个新数组并在其中填充数据)。下面演示了数组转型过程:

PRivate static void ArrayCasting() {// 创建一个二维FileStream数组FileStream[,] fs2dim = new FileStream[5, 10];// 隐式转型为一个二维Object数组Object[,] o2dim = fs2dim;// 不能从二维数组转型为一维数组//Stream[] s1dim = (Stream[]) o2dim;// 显式转型为二维Stream数组Stream[,] s2dim = (Stream[,]) o2dim;// 显式转型为二维String数组// 能通过编译,但在运行时会抛出异常String[,] st2dim = (String[,]) o2dim;// 创建一个意味Int32数组(元素是值类型)Int32[] i1dim = new Int32[5];// 不能将值类型的数组转型为其他任何类型// Object[] o1dim = (Object[]) i1dim;// 创建一个新数组,使用Array.Copy将元数组中的每一个元素// 转型为目标数组中的元素类型,并把它们复制过去// 下面的代码创建一个元素为引用类型的数组,// 每个元素都是对已装箱的Int32的引用Object[] o1dim = new Object[i1dim.Length];Array.Copy(i1dim, o1dim, 0);}

  Array.Copy方法的作用不仅仅是将元素从一个数组复制到另一个数组。Copy方法还能正确处理内存的重叠区域。

Copy方法还能在复制每一个数组元素时进行必要的类型转换。Copy方法能执行以下转换:1)将值类型的元素装箱为引用类型的元素,比如将一个Int32[]复制到一个Object[]中。2)将引用类型的元素拆箱为值类型的元素,比如将一个Object[]复制到Int32[]中。3)加宽CLR基元值类型,比如将一个Int32[]的元素复制到一个Double[]中。4)在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性。

  在某些情况下,将数组从一种类型转换为另一种类型是非常有用的。这种功能称为数据协变性。利用数组协变性时,应该清楚由此带来的性能损失。

  注意:如果只需要把数组中某些元素复制到另一个数组,可以选择System.Buffer的BlockCopy方法,它的执行速度比Array.Copy方法快。不过,Buffer的BlockCopy方法只支持基元类型,不提供像Array的Copy方法那样的转型能力。方法的Int32参数代表的是数组中的字节偏移量,而非元素索引。如果需要可靠的将一个数组中的元素复制到另一个数组,应该使用System.Array的ConstrainedCopy方法,该方法能保证不破坏目标数组中的数组的前提下完成复制,或者抛出异常。另外,它不执行任何装箱、拆箱或向下类型转换。

三、所有数组都隐式派生自System.Array

  如果像下面这样声明一个数组变量:

FileStream[] fsArray;

  CLR会为AppDomain自动创建一个FileStream[]类型。这个类型将隐式派生自System.Array类型;因此,System.Array类型定义的所有实例方法和属性都将有FileStream[]继承,使这些方法和属性能通过fsArray变量调用。

四、所有数组都隐式实现IEnumerable,ICollection和IList

  许多方法都能操作各种集合对象,因为在声明它们时,使用了IEnumerable,ICollection和IList等参数。可以将数组传给这些方法,因为System.Array也实现了这三个接口。System.Array之所以实现这些非泛型接口,是因为这些接口将所有元素都视为Systm.Object。然而,最好让System.Array实现这个接口的泛型形式,提供更好的编译时类型安全性和更好的性能。

五、数组的传递和返回  数组作为实参传给一个方法时,实际传递的是对该数组的引用。因此,被调用的方法能修改数组中的元素。如果不想被修改,必须生成数组的一个拷贝,并将这个拷贝传给方法。注意,Array.Copy方法执行的是浅拷贝。

  有的方法返回一个对数组的引用。如果方法构造并初始化数组,返回数组引用是没有问题的。但假如方法返回的是对一个字段维护的内部数组的引用,就必须决定是否向让该方法的调用者直接访问这个数组及其元素。如果是就可以返回数组引用。但是通常情况下,你并不希望方法的调用这获得这个访问权限。所以,方法应该构造一个新数组,并调用Array.Copy返回对新数组的一个引用。

  如果定义一个返回数组引用的方法,而且该数组不包含元素,那么方法既可以返回null,又可以放回对包含另个元素的一个数组的引用。实现这种方法时,Microsoft强烈建议让它返回后者,因为这样做能简化调用该方法时需要的代码。

// 这段代码更容易写,更容易理解Appointment[] app = GetAppointmentForToday();for (Int32 a =0; a< app.Length; a++) {// 对app[a]执行操作}

如果返回null的话:

// 写起来麻烦,不容易理解Appointment[] app = GetAppointmentForToday();if( app !=null ) {for (Int32 a =0; a< app.Length; a++) {// 对app[a]执行操作  }}

六、创建下限非零的数组

  可以调用数组的静态CreateInstance方法来动态创建自己的数组。该方法有若干个重载版本,允许指定数组元素的类型、数组的维数、每一维的下限和每一维的元素数目。CreateInstance为数组分配内存,将参数信息保存到数组的内存块的额外开销(overhead)部分。然后返回对该数组的一个引用。

七、数组的访问性能  CLR内部实际支持两种不同的数组  1)下限为0的意味数组。这些数组有时称为SZ数组或向量。  2)下限未知的一维或多维数组。  可执行一下代码来实际地查看不同种类的输出

internal static class ArrayTypes {public static void Go() {Array a;// 创建一个一维数组的0基数组,其中不包含任何元素a = new String[0];  Console.WriteLine(a.GetType());    // System.String[]// 创建一个一维数组的0基数组,其中不包含任何元素a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 0 });  Console.WriteLine(a.GetType());    // System.String[]// 创建一个一维数组的1基数组,其中不包含任何元素a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 1 });  Console.WriteLine(a.GetType());    // System.String[*] <-- 注意!Console.WriteLine();// 创建一个二维数组的0基数组,其中不包含任何元素a = new String[0, 0];  Console.WriteLine(a.GetType());    // System.String[,]// 创建一个二维数组的0基数组,其中不包含任何元素a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 0, 0 });  Console.WriteLine(a.GetType());    // System.String[,]// 创建一个二维数组的1基数组,其中不包含任何元素a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 1, 1 });  Console.WriteLine(a.GetType());    // System.String[,]  }}

  对于一维数组,0基数组显示的类型名称是System.String[],但1基数组显示的是System.String[*]。*符号表示CLR知道该数组不是0基的。注意,C#不允许声明String[*]类型的变量,因此不能使用C#语法来访问一维的非0基数组。尽管可以调用Array的GetValue和SetValue方法来访问数组的元素,但速度会比较慢,毕竟有方法调用的开销。

  对于多维数组,0基和1基数组会显示同样的类型名称:System.String[,]。在运行时,CLR将对所有多维数组都视为非0基数组。这自然会人觉得应该显示为System.String[*,*]。但是,对于多维数组,CLR决定不用*符号,避免开发人员对*产生混淆。  访问一维0基数组的元素比访问非0基数组或多维数组的元素稍快一些。首先,有一些特殊的IL指令,比如newarr,ldelem,ldelema等用于处理一维0基数组,这些特殊IL指令会导致JIT编译器生成优化代码。其次,JIT编译器知道for循环要反问0到Length-1之间的数组元素。所以,JIT编译器生成的代码会在运行时测试所有数组元素的访问都在数组有效访问内。

  如果很关系性能,请考虑由数组构成的数组(即交错数组)来替代矩形数组。

  下面C#代码演示了访问二维数组的三种方式:

internal static class MultiDimArrayPerformance{private const Int32 c_numElements = 10000;public static void Go(){const Int32 testCount = 10;Stopwatch sw;// 声明一个二维数组Int32[,] a2Dim = new Int32[c_numElements, c_numElements];// 将一个二维数组声明为交错数组Int32[][] aJagged = new Int32[c_numElements][];for (Int32 x = 0; x < c_numElements; x++)aJagged[x] = new Int32[c_numElements];// 1: 用普通的安全技术访问数组中的所有元素sw = Stopwatch.StartNew();for (Int32 test = 0; test < testCount; test++)Safe2DimArrayaccess(a2Dim);Console.WriteLine("{0}: Safe2DimArrayAccess", sw.Elapsed);// 2: 用交错数组技术访问数组中的所有元素sw = Stopwatch.StartNew();for (Int32 test = 0; test < testCount; test++)SafeJaggedArrayAccess(aJagged);Console.WriteLine("{0}: SafeJaggedArrayAccess", sw.Elapsed);// 3: 用unsafe访问数组中的所有元素sw = Stopwatch.StartNew();for (Int32 test = 0; test < testCount; test++)Unsafe2DimArrayAccess(a2Dim);Console.WriteLine("{0}: Unsafe2DimArrayAccess", sw.Elapsed);Console.ReadLine();}private static Int32 Safe2DimArrayAccess(Int32[,] a){  Int32 sum = 0;  for (Int32 x = 0; x < c_numElements; x++)  {    for (Int32 y = 0; y < c_numElements; y++)    {      sum += a[x, y];    }  }  return sum;}private static Int32 SafeJaggedArrayAccess(Int32[][] a){  Int32 sum = 0;  for (Int32 x = 0; x < c_numElements; x++)  {    for (Int32 y = 0; y < c_numEleme
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表